diff --git a/.gitignore b/.gitignore index b5a46e16f4..7d672c7755 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,7 @@ ClientBin/ *.[Pp]ublish.xml *.pfx *.publishsettings +Events_Avalonia.cs # RIA/Silverlight projects Generated_Code/ diff --git a/.ncrunch/Avalonia.Controls.UnitTests.net47.v3.ncrunchproject b/.ncrunch/Avalonia.Controls.UnitTests.net47.v3.ncrunchproject index e9d39b0c74..f30a20df78 100644 --- a/.ncrunch/Avalonia.Controls.UnitTests.net47.v3.ncrunchproject +++ b/.ncrunch/Avalonia.Controls.UnitTests.net47.v3.ncrunchproject @@ -3,5 +3,6 @@ MissingOrIgnoredProjectReference + Avalonia.Controls.UnitTests.TimePickerTests \ No newline at end of file diff --git a/.ncrunch/Sandbox.v3.ncrunchproject b/.ncrunch/Sandbox.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Sandbox.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index 922c8f57dd..34ad19b41d 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -222,6 +222,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -2012,6 +2016,54 @@ Global {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.Build.0 = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.ActiveCfg = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|Any CPU.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhone.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhone.Build.0 = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2070,6 +2122,7 @@ Global {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/Avalonia.v3.ncrunchsolution b/Avalonia.v3.ncrunchsolution index bef7e45524..afce1018ec 100644 --- a/Avalonia.v3.ncrunchsolution +++ b/Avalonia.v3.ncrunchsolution @@ -6,6 +6,9 @@ src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll True + + RunApiCompat = false + .ncrunch True diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 54645e461e..721a0415f4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -35,16 +35,9 @@ jobs: vmImage: 'macOS-10.14' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.101' + displayName: 'Use .NET Core SDK 3.1.401' inputs: - packageType: sdk - version: 3.1.101 - - - task: UseDotNet@2 - displayName: 'Use .NET Core Runtime 3.1.1' - inputs: - packageType: runtime - version: 3.1.1 + version: 3.1.401 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -63,13 +56,6 @@ jobs: xcodeVersion: '10' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - - task: CmdLine@2 - displayName: 'Install CastXML' - inputs: - script: | - brew update - brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb - - task: CmdLine@2 displayName: 'Install Nuke' inputs: @@ -88,7 +74,7 @@ jobs: export PATH="$PATH:$HOME/.dotnet/tools" dotnet --info printenv - nuke --target CiAzureOSX --configuration Release + nuke --target CiAzureOSX --configuration Release --skip-previewer - task: PublishTestResults@2 inputs: @@ -112,6 +98,11 @@ jobs: pool: vmImage: 'windows-2019' steps: + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 3.1.401' + inputs: + version: 3.1.401 + - task: CmdLine@2 displayName: 'Install Nuke' inputs: diff --git a/build/NetFX.props b/build/NetFX.props index ed5cb6dd69..8ffc9ec561 100644 --- a/build/NetFX.props +++ b/build/NetFX.props @@ -1,7 +1,7 @@  - + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index d7d04c7971..f2e7df36cd 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/dirs.proj b/dirs.proj index a48ad6e03d..bf32abef72 100644 --- a/dirs.proj +++ b/dirs.proj @@ -7,6 +7,7 @@ + diff --git a/global.json b/global.json index 128511eb48..b2b2da7c4f 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.101" + "version": "3.1.401" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm index 1565417c1a..93a33bbbb0 100644 --- a/native/Avalonia.Native/src/OSX/rendertarget.mm +++ b/native/Avalonia.Native/src/OSX/rendertarget.mm @@ -110,7 +110,7 @@ if(_renderbuffer != 0) glDeleteRenderbuffers(1, &_renderbuffer); } - IOSurfaceDecrementUseCount(surface); + CFRelease(surface); } @end diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index fbfbf47e1b..097815cc69 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; using System.Xml.Linq; using Nuke.Common; using Nuke.Common.Git; @@ -15,6 +16,7 @@ using Nuke.Common.Tools.MSBuild; using Nuke.Common.Tools.Npm; using Nuke.Common.Utilities; using Nuke.Common.Utilities.Collections; +using Pharmacist.Core; using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; @@ -124,6 +126,7 @@ partial class Build : NukeBuild Target CompileHtmlPreviewer => _ => _ .DependsOn(Clean) + .OnlyWhenStatic(() => !Parameters.SkipPreviewer) .Executes(() => { var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp"; @@ -135,11 +138,21 @@ partial class Build : NukeBuild .SetWorkingDirectory(webappDir) .SetCommand("dist")); }); - - Target Compile => _ => _ + + Target CompileNative => _ => _ .DependsOn(Clean) - .DependsOn(CompileHtmlPreviewer) + .OnlyWhenStatic(() => EnvironmentInfo.IsOsx) .Executes(() => + { + var project = $"{RootDirectory}/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/"; + var args = $"-project {project} -configuration {Parameters.Configuration} CONFIGURATION_BUILD_DIR={RootDirectory}/Build/Products/Release"; + ProcessTasks.StartProcess("xcodebuild", args).AssertZeroExitCode(); + }); + + Target Compile => _ => _ + .DependsOn(Clean, CompileNative) + .DependsOn(CompileHtmlPreviewer) + .Executes(async () => { if (Parameters.IsRunningOnWindows) MsBuildCommon(Parameters.MSBuildSolution, c => c @@ -153,8 +166,44 @@ partial class Build : NukeBuild .AddProperty("PackageVersion", Parameters.Version) .SetConfiguration(Parameters.Configuration) ); + + await CompileReactiveEvents(); }); + async Task CompileReactiveEvents() + { + var avaloniaBuildOutput = Path.Combine(RootDirectory, "packages", "Avalonia", "bin", Parameters.Configuration); + var avaloniaAssemblies = GlobFiles(avaloniaBuildOutput, "**/Avalonia*.dll") + .Where(file => !file.Contains("Avalonia.Build.Tasks") && + !file.Contains("Avalonia.Remote.Protocol")); + + var eventsDirectory = GlobDirectories($"{RootDirectory}/src/**/Avalonia.ReactiveUI.Events").First(); + var eventsBuildFile = Path.Combine(eventsDirectory, "Events_Avalonia.cs"); + if (File.Exists(eventsBuildFile)) + File.Delete(eventsBuildFile); + + using (var stream = File.Create(eventsBuildFile)) + using (var writer = new StreamWriter(stream)) + { + await ObservablesForEventGenerator.ExtractEventsFromAssemblies( + writer, avaloniaAssemblies, new string[0], "netstandard2.0" + ); + } + + var eventsProject = Path.Combine(eventsDirectory, "Avalonia.ReactiveUI.Events.csproj"); + if (Parameters.IsRunningOnWindows) + MsBuildCommon(eventsProject, c => c + .SetArgumentConfigurator(a => a.Add("/r")) + .AddTargets("Build") + ); + else + DotNetBuild(c => c + .SetProjectFile(eventsProject) + .AddProperty("PackageVersion", Parameters.Version) + .SetConfiguration(Parameters.Configuration) + ); + } + void RunCoreTest(string projectName) { Information($"Running tests from {projectName}"); diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 149716b416..a167e9d892 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -19,10 +19,14 @@ public partial class Build [Parameter("force-nuget-version")] public string ForceNugetVersion { get; set; } + [Parameter("skip-previewer")] + public bool SkipPreviewer { get; set; } + public class BuildParameters { public string Configuration { get; } public bool SkipTests { get; } + public bool SkipPreviewer {get;} public string MainRepo { get; } public string MasterBranch { get; } public string RepositoryName { get; } @@ -63,6 +67,7 @@ public partial class Build // ARGUMENTS Configuration = b.Configuration ?? "Release"; SkipTests = b.SkipTests; + SkipPreviewer = b.SkipPreviewer; // CONFIGURATION MainRepo = "https://github.com/AvaloniaUI/Avalonia"; diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 4c64d4ff93..b06e49f2eb 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -17,6 +17,7 @@ + diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index efc90357ed..790813fda0 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -45,7 +45,10 @@ - + + + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index f90a0c4658..a49616e543 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -51,6 +51,11 @@ Width="200" Margin="0,0,0,8" FilterMode="None"/> + + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs index f9d6a72a3a..574cc79a7d 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs @@ -92,13 +92,28 @@ namespace ControlCatalog.Pages } public StateData[] States { get; private set; } + private LinkedList[] BuildAllSentences() + { + return new string[] + { + "Hello world", + "No this is Patrick", + "Never gonna give you up", + "How does one patch KDE2 under FreeBSD" + } + .Select(x => new LinkedList(x.Split(' '))) + .ToArray(); + } + public LinkedList[] Sentences { get; private set; } + public AutoCompleteBoxPage() { this.InitializeComponent(); States = BuildAllStates(); + Sentences = BuildAllSentences(); - foreach (AutoCompleteBox box in GetAllAutoCompleteBox()) + foreach (AutoCompleteBox box in GetAllAutoCompleteBox().Where(x => x.Name != "CustomAutocompleteBox")) { box.Items = States; } @@ -116,6 +131,11 @@ namespace ControlCatalog.Pages var asyncBox = this.FindControl("AsyncBox"); asyncBox.AsyncPopulator = PopulateAsync; + + var customAutocompleteBox = this.FindControl("CustomAutocompleteBox"); + customAutocompleteBox.Items = Sentences.SelectMany(x => x); + customAutocompleteBox.TextFilter = LastWordContains; + customAutocompleteBox.TextSelector = AppendWord; } private IEnumerable GetAllAutoCompleteBox() { @@ -137,6 +157,42 @@ namespace ControlCatalog.Pages .ToList(); } + private bool LastWordContains(string searchText, string item) + { + var words = searchText.Split(' '); + var options = Sentences.Select(x => x.First).ToArray(); + for (var i = 0; i < words.Length; ++i) + { + var word = words[i]; + for (var j = 0; j < options.Length; ++j) + { + var option = options[j]; + if (option == null) + continue; + + if (i == words.Length - 1) + { + options[j] = option.Value.ToLower().Contains(word.ToLower()) ? option : null; + } + else + { + options[j] = option.Value.Equals(word, StringComparison.InvariantCultureIgnoreCase) ? option.Next : null; + } + } + } + + return options.Any(x => x != null && x.Value == item); + } + private string AppendWord(string text, string item) + { + string[] parts = text.Split(' '); + if (parts.Length == 0) + return item; + + parts[parts.Length - 1] = item; + return string.Join(" ", parts); + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs index 0b7fb12aff..2a30f4d91b 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs @@ -1,8 +1,12 @@ +using System.Collections; using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; using Avalonia.Controls; using Avalonia.Markup.Xaml; using ControlCatalog.Models; using Avalonia.Collections; +using Avalonia.Data; namespace ControlCatalog.Pages { @@ -11,12 +15,22 @@ namespace ControlCatalog.Pages public DataGridPage() { this.InitializeComponent(); + + var dataGridSortDescription = DataGridSortDescription.FromPath(nameof(Country.Region), ListSortDirection.Ascending, new ReversedStringComparer()); + var collectionView1 = new DataGridCollectionView(Countries.All); + collectionView1.SortDescriptions.Add(dataGridSortDescription); var dg1 = this.FindControl("dataGrid1"); dg1.IsReadOnly = true; dg1.LoadingRow += Dg1_LoadingRow; - var collectionView1 = new DataGridCollectionView(Countries.All); - //collectionView.GroupDescriptions.Add(new PathGroupDescription("Region")); - + dg1.Sorting += (s, a) => + { + var property = ((a.Column as DataGridBoundColumn)?.Binding as Binding).Path; + if (property == dataGridSortDescription.PropertyPath + && !collectionView1.SortDescriptions.Contains(dataGridSortDescription)) + { + collectionView1.SortDescriptions.Add(dataGridSortDescription); + } + }; dg1.Items = collectionView1; var dg2 = this.FindControl("dataGridGrouping"); @@ -53,5 +67,20 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); } + + private class ReversedStringComparer : IComparer, IComparer + { + public int Compare(object x, object y) + { + if (x is string left && y is string right) + { + var reversedLeft = new string(left.Reverse().ToArray()); + var reversedRight = new string(right.Reverse().ToArray()); + return reversedLeft.CompareTo(reversedRight); + } + + return Comparer.Default.Compare(x, y); + } + } } } diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index edf3d41bf5..3521ad71a9 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -1,35 +1,25 @@ - - ListBox - Hosts a collection of ListBoxItem. - - - - - - - - - - - - - Single - Multiple - Toggle - AlwaysSelected - - + + + ListBox + Hosts a collection of ListBoxItem. - + + Multiple + Toggle + AlwaysSelected + AutoScrollToSelectedItem + + + + + + + + diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs index 6c13a5ac22..cb79bf219a 100644 --- a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; using Avalonia.OpenGL; +using Avalonia.OpenGL.Controls; using Avalonia.Platform.Interop; using Avalonia.Threading; using static Avalonia.OpenGL.GlConsts; diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs index d088576998..f75bc32105 100644 --- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -10,22 +10,39 @@ namespace ControlCatalog.ViewModels { public class ListBoxPageViewModel : ReactiveObject { + private bool _multiple; + private bool _toggle; + private bool _alwaysSelected; + private bool _autoScrollToSelectedItem = true; private int _counter; - private SelectionMode _selectionMode; + private ObservableAsPropertyHelper _selectionMode; public ListBoxPageViewModel() { Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem())); + Selection = new SelectionModel(); Selection.Select(1); + _selectionMode = this.WhenAnyValue( + x => x.Multiple, + x => x.Toggle, + x => x.AlwaysSelected, + (m, t, a) => + (m ? SelectionMode.Multiple : 0) | + (t ? SelectionMode.Toggle : 0) | + (a ? SelectionMode.AlwaysSelected : 0)) + .ToProperty(this, x => x.SelectionMode); + AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); RemoveItemCommand = ReactiveCommand.Create(() => { - while (Selection.Count > 0) + var items = Selection.SelectedItems.ToList(); + + foreach (var item in items) { - Items.Remove(Selection.SelectedItems.First()); + Items.Remove(item); } }); @@ -42,25 +59,37 @@ namespace ControlCatalog.ViewModels } public ObservableCollection Items { get; } - public SelectionModel Selection { get; } + public SelectionMode SelectionMode => _selectionMode.Value; - public ReactiveCommand AddItemCommand { get; } + public bool Multiple + { + get => _multiple; + set => this.RaiseAndSetIfChanged(ref _multiple, value); + } - public ReactiveCommand RemoveItemCommand { get; } + public bool Toggle + { + get => _toggle; + set => this.RaiseAndSetIfChanged(ref _toggle, value); + } - public ReactiveCommand SelectRandomItemCommand { get; } + public bool AlwaysSelected + { + get => _alwaysSelected; + set => this.RaiseAndSetIfChanged(ref _alwaysSelected, value); + } - public SelectionMode SelectionMode + public bool AutoScrollToSelectedItem { - get => _selectionMode; - set - { - Selection.Clear(); - this.RaiseAndSetIfChanged(ref _selectionMode, value); - } + get => _autoScrollToSelectedItem; + set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value); } + public ReactiveCommand AddItemCommand { get; } + public ReactiveCommand RemoveItemCommand { get; } + public ReactiveCommand SelectRandomItemCommand { get; } + private string GenerateItem() => $"Item {_counter++.ToString()}"; } } diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 770960d7c4..93fbe5e412 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -3,8 +3,8 @@ x:Class="RenderDemo.MainWindow" Title="AvaloniaUI Rendering Test" xmlns:pages="clr-namespace:RenderDemo.Pages" - Width="800" - Height="600"> + Width="{Binding Width, Mode=TwoWay}" + Height="{Binding Height, Mode=TwoWay}"> @@ -24,6 +24,10 @@ + + + diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs index 7f15845596..ddee880288 100644 --- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs @@ -61,7 +61,6 @@ namespace RenderDemo.Pages { Foreground = Brushes.Black, GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices), - BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale) }; drawingGroup.Children.Add(glyphRunDrawing); @@ -69,7 +68,7 @@ namespace RenderDemo.Pages var geometryDrawing = new GeometryDrawing { Pen = new Pen(Brushes.Black), - Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds } + Geometry = new RectangleGeometry { Rect = new Rect(glyphRunDrawing.GlyphRun.Size) } }; drawingGroup.Children.Add(geometryDrawing); diff --git a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs index d2d789a687..eda5e80530 100644 --- a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,6 @@ -using System; -using System.Reactive; +using System.Reactive; +using System.Threading.Tasks; + using ReactiveUI; namespace RenderDemo.ViewModels @@ -8,26 +9,61 @@ namespace RenderDemo.ViewModels { private bool drawDirtyRects = false; private bool drawFps = true; + private double width = 800; + private double height = 600; public MainWindowViewModel() { ToggleDrawDirtyRects = ReactiveCommand.Create(() => DrawDirtyRects = !DrawDirtyRects); ToggleDrawFps = ReactiveCommand.Create(() => DrawFps = !DrawFps); + ResizeWindow = ReactiveCommand.CreateFromTask(ResizeWindowAsync); } public bool DrawDirtyRects { - get { return drawDirtyRects; } - set { this.RaiseAndSetIfChanged(ref drawDirtyRects, value); } + get => drawDirtyRects; + set => this.RaiseAndSetIfChanged(ref drawDirtyRects, value); } public bool DrawFps { - get { return drawFps; } - set { this.RaiseAndSetIfChanged(ref drawFps, value); } + get => drawFps; + set => this.RaiseAndSetIfChanged(ref drawFps, value); + } + + public double Width + { + get => width; + set => this.RaiseAndSetIfChanged(ref width, value); + } + + public double Height + { + get => height; + set => this.RaiseAndSetIfChanged(ref height, value); } public ReactiveCommand ToggleDrawDirtyRects { get; } public ReactiveCommand ToggleDrawFps { get; } + public ReactiveCommand ResizeWindow { get; } + + private async Task ResizeWindowAsync() + { + for (int i = 0; i < 30; i++) + { + Width += 10; + Height += 5; + await Task.Delay(10); + } + + await Task.Delay(10); + + for (int i = 0; i < 30; i++) + { + Width -= 10; + Height -= 5; + await Task.Delay(10); + } + } } } diff --git a/samples/Sandbox/App.axaml b/samples/Sandbox/App.axaml new file mode 100644 index 0000000000..699781eb94 --- /dev/null +++ b/samples/Sandbox/App.axaml @@ -0,0 +1,8 @@ + + + + + diff --git a/samples/Sandbox/App.axaml.cs b/samples/Sandbox/App.axaml.cs new file mode 100644 index 0000000000..7eb8345784 --- /dev/null +++ b/samples/Sandbox/App.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace Sandbox +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { + desktopLifetime.MainWindow = new MainWindow(); + } + } + } +} diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml new file mode 100644 index 0000000000..6929f192c7 --- /dev/null +++ b/samples/Sandbox/MainWindow.axaml @@ -0,0 +1,4 @@ + + diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs new file mode 100644 index 0000000000..b7222e043d --- /dev/null +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sandbox +{ + public class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + this.AttachDevTools(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/Sandbox/Program.cs b/samples/Sandbox/Program.cs new file mode 100644 index 0000000000..4d7eda8d9f --- /dev/null +++ b/samples/Sandbox/Program.cs @@ -0,0 +1,17 @@ +using Avalonia; +using Avalonia.ReactiveUI; + +namespace Sandbox +{ + public class Program + { + static void Main(string[] args) + { + AppBuilder.Configure() + .UsePlatformDetect() + .UseReactiveUI() + .LogToDebug() + .StartWithClassicDesktopLifetime(args); + } + } +} diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj new file mode 100644 index 0000000000..1a0a8a7ce5 --- /dev/null +++ b/samples/Sandbox/Sandbox.csproj @@ -0,0 +1,18 @@ + + + + WinExe + netcoreapp3.1 + true + + + + + + + + + + + + diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt new file mode 100644 index 0000000000..4668a572c5 --- /dev/null +++ b/src/Avalonia.Base/ApiCompatBaseline.txt @@ -0,0 +1,3 @@ +Compat issues with assembly Avalonia.Base: +CannotAddAbstractMembers : Member 'protected System.IObservable Avalonia.AvaloniaProperty.GetChanged()' is abstract in the implementation but is missing in the contract. +Total Issues: 1 diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 39391490b0..3ae0445e9b 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Utilities; @@ -18,7 +17,6 @@ namespace Avalonia public static readonly object UnsetValue = new UnsetValueType(); private static int s_nextId; - private readonly Subject _changed; private readonly PropertyMetadata _defaultMetadata; private readonly Dictionary _metadata; private readonly Dictionary _metadataCache = new Dictionary(); @@ -50,7 +48,6 @@ namespace Avalonia throw new ArgumentException("'name' may not contain periods."); } - _changed = new Subject(); _metadata = new Dictionary(); Name = name; @@ -77,7 +74,6 @@ namespace Avalonia Contract.Requires(source != null); Contract.Requires(ownerType != null); - _changed = source._changed; _metadata = new Dictionary(); Name = source.Name; @@ -139,7 +135,7 @@ namespace Avalonia /// An observable that is fired when this property changes on any /// instance. /// - public IObservable Changed => _changed; + public IObservable Changed => GetChanged(); /// /// Gets a method that gets called before and after the property starts being notified on an @@ -474,15 +470,6 @@ namespace Avalonia public abstract void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) where TData : struct; - /// - /// Notifies the observable. - /// - /// The observable arguments. - internal void NotifyChanged(AvaloniaPropertyChangedEventArgs e) - { - _changed.OnNext(e); - } - /// /// Routes an untyped ClearValue call to a typed call. /// @@ -553,6 +540,8 @@ namespace Avalonia _hasMetadataOverrides = true; } + protected abstract IObservable GetChanged(); + private PropertyMetadata GetMetadataWithOverrides(Type type) { if (type is null) diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs index 2f26d855f2..d5549e979b 100644 --- a/src/Avalonia.Base/AvaloniaProperty`1.cs +++ b/src/Avalonia.Base/AvaloniaProperty`1.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Utilities; @@ -10,6 +11,8 @@ namespace Avalonia /// The value type of the property. public abstract class AvaloniaProperty : AvaloniaProperty { + private readonly Subject> _changed; + /// /// Initializes a new instance of the class. /// @@ -24,22 +27,61 @@ namespace Avalonia Action notifying = null) : base(name, typeof(TValue), ownerType, metadata, notifying) { + _changed = new Subject>(); } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The property to copy. /// The new owner type. /// Optional overridden metadata. + [Obsolete("Use constructor with AvaloniaProperty instead.", true)] protected AvaloniaProperty( - AvaloniaProperty source, - Type ownerType, + AvaloniaProperty source, + Type ownerType, + PropertyMetadata metadata) + : this(source as AvaloniaProperty ?? throw new InvalidOperationException(), ownerType, metadata) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The property to copy. + /// The new owner type. + /// Optional overridden metadata. + protected AvaloniaProperty( + AvaloniaProperty source, + Type ownerType, PropertyMetadata metadata) : base(source, ownerType, metadata) { + _changed = source._changed; + } + + /// + /// Gets an observable that is fired when this property changes on any + /// instance. + /// + /// + /// An observable that is fired when this property changes on any + /// instance. + /// + + public new IObservable> Changed => _changed; + + /// + /// Notifies the observable. + /// + /// The observable arguments. + internal void NotifyChanged(AvaloniaPropertyChangedEventArgs e) + { + _changed.OnNext(e); } + protected override IObservable GetChanged() => Changed; + protected BindingValue TryConvert(object value) { if (value == UnsetValue) diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index f201cfab1f..d43b4e04bb 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -543,7 +543,73 @@ namespace Avalonia.Collections /// void ICollection.CopyTo(Array array, int index) { - _inner.CopyTo((T[])array, index); + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (array.Rank != 1) + { + throw new ArgumentException("Multi-dimensional arrays are not supported."); + } + + if (array.GetLowerBound(0) != 0) + { + throw new ArgumentException("Non-zero lower bounds are not supported."); + } + + if (index < 0) + { + throw new ArgumentException("Invalid index."); + } + + if (array.Length - index < Count) + { + throw new ArgumentException("The target array is too small."); + } + + if (array is T[] tArray) + { + _inner.CopyTo(tArray, index); + } + else + { + // + // Catch the obvious case assignment will fail. + // We can't find all possible problems by doing the check though. + // For example, if the element type of the Array is derived from T, + // we can't figure out if we can successfully copy the element beforehand. + // + Type targetType = array.GetType().GetElementType()!; + Type sourceType = typeof(T); + if (!(targetType.IsAssignableFrom(sourceType) || sourceType.IsAssignableFrom(targetType))) + { + throw new ArgumentException("Invalid array type"); + } + + // + // We can't cast array of value type to object[], so we don't support + // widening of primitive types here. + // + object[] objects = array as object[]; + if (objects == null) + { + throw new ArgumentException("Invalid array type"); + } + + int count = _inner.Count; + try + { + for (int i = 0; i < count; i++) + { + objects[index++] = _inner[i]; + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException("Invalid array type"); + } + } } /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index dbc2625b86..a2f113adb7 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -32,15 +32,30 @@ namespace Avalonia } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The property to copy. /// The new owner type. /// Optional overridden metadata. + [Obsolete("Use constructor with DirectPropertyBase instead.", true)] protected DirectPropertyBase( AvaloniaProperty source, Type ownerType, PropertyMetadata metadata) + : this(source as DirectPropertyBase ?? throw new InvalidOperationException(), ownerType, metadata) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The property to copy. + /// The new owner type. + /// Optional overridden metadata. + protected DirectPropertyBase( + DirectPropertyBase source, + Type ownerType, + PropertyMetadata metadata) : base(source, ownerType, metadata) { } diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs index f741d40571..662ff91329 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs @@ -1,19 +1,21 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; -using System.Text; -using Avalonia.Controls; using Avalonia.Controls.Utils; -using Avalonia.Utilities; namespace Avalonia.Collections { public abstract class DataGridSortDescription { public virtual string PropertyPath => null; - public virtual bool Descending => false; + + [Obsolete("Use Direction property to read or override sorting direction.")] + public virtual bool Descending => Direction == ListSortDirection.Descending; + + public virtual ListSortDirection Direction => ListSortDirection.Ascending; public bool HasPropertyPath => !String.IsNullOrEmpty(PropertyPath); public abstract IComparer Comparer { get; } @@ -26,7 +28,7 @@ namespace Avalonia.Collections return seq.ThenBy(o => o, Comparer); } - internal virtual DataGridSortDescription SwitchSortDirection() + public virtual DataGridSortDescription SwitchSortDirection() { return this; } @@ -105,7 +107,7 @@ namespace Avalonia.Collections private class DataGridPathSortDescription : DataGridSortDescription { - private readonly bool _descending; + private readonly ListSortDirection _direction; private readonly string _propertyPath; private readonly Lazy _cultureSensitiveComparer; private readonly Lazy> _comparer; @@ -118,7 +120,7 @@ namespace Avalonia.Collections { if (_internalComparerTyped == null && _internalComparer != null) { - if (_internalComparerTyped is IComparer c) + if (_internalComparer is IComparer c) _internalComparerTyped = c; else _internalComparerTyped = Comparer.Create((x, y) => _internalComparer.Compare(x, y)); @@ -130,19 +132,20 @@ namespace Avalonia.Collections public override string PropertyPath => _propertyPath; public override IComparer Comparer => _comparer.Value; - public override bool Descending => _descending; + public override ListSortDirection Direction => _direction; - public DataGridPathSortDescription(string propertyPath, bool descending, CultureInfo culture) + public DataGridPathSortDescription(string propertyPath, ListSortDirection direction, IComparer internalComparer, CultureInfo culture) { _propertyPath = propertyPath; - _descending = descending; + _direction = direction; _cultureSensitiveComparer = new Lazy(() => new CultureSensitiveComparer(culture ?? CultureInfo.CurrentCulture)); + _internalComparer = internalComparer; _comparer = new Lazy>(() => Comparer.Create((x, y) => Compare(x, y))); } - private DataGridPathSortDescription(DataGridPathSortDescription inner, bool descending) + private DataGridPathSortDescription(DataGridPathSortDescription inner, ListSortDirection direction) { _propertyPath = inner._propertyPath; - _descending = descending; + _direction = direction; _propertyType = inner._propertyType; _cultureSensitiveComparer = inner._cultureSensitiveComparer; _internalComparer = inner._internalComparer; @@ -201,7 +204,7 @@ namespace Avalonia.Collections result = _internalComparer?.Compare(v1, v2) ?? 0; - if (_descending) + if (Direction == ListSortDirection.Descending) return -result; else return result; @@ -218,7 +221,7 @@ namespace Avalonia.Collections } public override IOrderedEnumerable OrderBy(IEnumerable seq) { - if(_descending) + if (Direction == ListSortDirection.Descending) { return seq.OrderByDescending(o => GetValue(o), InternalComparer); } @@ -229,7 +232,7 @@ namespace Avalonia.Collections } public override IOrderedEnumerable ThenBy(IOrderedEnumerable seq) { - if (_descending) + if (Direction == ListSortDirection.Descending) { return seq.ThenByDescending(o => GetValue(o), InternalComparer); } @@ -239,15 +242,28 @@ namespace Avalonia.Collections } } - internal override DataGridSortDescription SwitchSortDirection() + public override DataGridSortDescription SwitchSortDirection() { - return new DataGridPathSortDescription(this, !_descending); + var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending; + return new DataGridPathSortDescription(this, newDirection); } } - public static DataGridSortDescription FromPath(string propertyPath, bool descending = false, CultureInfo culture = null) + public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction = ListSortDirection.Ascending, CultureInfo culture = null) + { + return new DataGridPathSortDescription(propertyPath, direction, null, culture); + } + + + [Obsolete("Use overload taking a ListSortDirection.")] + public static DataGridSortDescription FromPath(string propertyPath, bool descending, CultureInfo culture = null) + { + return new DataGridPathSortDescription(propertyPath, descending ? ListSortDirection.Descending : ListSortDirection.Ascending, null, culture); + } + + public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction, IComparer comparer) { - return new DataGridPathSortDescription(propertyPath, descending, culture); + return new DataGridPathSortDescription(propertyPath, direction, comparer, null); } } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index f7903086ab..7c57ea3db9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -24,12 +24,14 @@ using Avalonia.Input.Platform; using System.ComponentModel.DataAnnotations; using Avalonia.Controls.Utils; using Avalonia.Layout; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// Displays data in a customizable grid. /// + [PseudoClasses(":invalid")] public partial class DataGrid : TemplatedControl { private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter"; @@ -1229,6 +1231,11 @@ namespace Avalonia.Controls remove { AddHandler(SelectionChangedEvent, value); } } + /// + /// Occurs when the sorting request is triggered. + /// + public event EventHandler Sorting; + /// /// Occurs when a /// object becomes available for reuse. diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 8e82bf1a38..1e72a07760 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -49,8 +49,12 @@ namespace Avalonia.Controls { if(_binding is Avalonia.Data.Binding binding) { - // Force the TwoWay binding mode if there is a Path present. TwoWay binding requires a Path. - if (!String.IsNullOrEmpty(binding.Path)) + if (binding.Mode == BindingMode.OneWayToSource) + { + throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource. Use BindingMode.TwoWay instead."); + } + + if (!String.IsNullOrEmpty(binding.Path) && binding.Mode == BindingMode.Default) { binding.Mode = BindingMode.TwoWay; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index e5fbfa1a81..445dc541a7 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Input; @@ -12,6 +13,7 @@ namespace Avalonia.Controls /// /// Represents an individual cell. /// + [PseudoClasses(":selected", ":current", ":edited", ":invalid")] public class DataGridCell : ContentControl { private const string DATAGRIDCELL_elementRightGridLine = "PART_RightGridLine"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 128fbde0c1..23c4acdf6c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -1047,4 +1047,4 @@ namespace Avalonia.Controls } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 017718bc92..856d1f6566 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -14,12 +14,14 @@ using Avalonia.Utilities; using System; using Avalonia.Controls.Utils; using Avalonia.Controls.Mixins; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { /// /// Represents an individual column header. /// + [PseudoClasses(":dragIndicator", ":pressed", ":sortascending", ":sortdescending")] public class DataGridColumnHeader : ContentControl { private enum DragMode @@ -161,13 +163,14 @@ namespace Avalonia.Controls var sort = OwningColumn.GetSortDescription(); if (sort != null) { - CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending; + CurrentSortingState = sort.Direction; } } + PseudoClasses.Set(":sortascending", - CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending); + CurrentSortingState == ListSortDirection.Ascending); PseudoClasses.Set(":sortdescending", - CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending); + CurrentSortingState == ListSortDirection.Descending); } internal void UpdateSeparatorVisibility(DataGridColumn lastVisibleColumn) @@ -215,70 +218,76 @@ namespace Avalonia.Controls internal void ProcessSort(KeyModifiers keyModifiers) { // if we can sort: - // - DataConnection.AllowSort is true, and // - AllowUserToSortColumns and CanSort are true, and - // - OwningColumn is bound, and - // - SortDescriptionsCollection exists, and - // - the column's data type is comparable + // - OwningColumn is bound // then try to sort if (OwningColumn != null && OwningGrid != null && OwningGrid.EditingRow == null && OwningColumn != OwningGrid.ColumnsInternal.FillerColumn - && OwningGrid.DataConnection.AllowSort && OwningGrid.CanUserSortColumns - && OwningColumn.CanUserSort - && OwningGrid.DataConnection.SortDescriptions != null) + && OwningColumn.CanUserSort) { - DataGrid owningGrid = OwningGrid; + var ea = new DataGridColumnEventArgs(OwningColumn); + OwningGrid.OnColumnSorting(ea); - DataGridSortDescription newSort; + if (!ea.Handled && OwningGrid.DataConnection.AllowSort && OwningGrid.DataConnection.SortDescriptions != null) + { + // - DataConnection.AllowSort is true, and + // - SortDescriptionsCollection exists, and + // - the column's data type is comparable - KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift); + DataGrid owningGrid = OwningGrid; + DataGridSortDescription newSort; - DataGridSortDescription sort = OwningColumn.GetSortDescription(); - IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView; - Debug.Assert(collectionView != null); - using (collectionView.DeferRefresh()) - { - // if shift is held down, we multi-sort, therefore if it isn't, we'll clear the sorts beforehand - if (!shift || owningGrid.DataConnection.SortDescriptions.Count == 0) - { - owningGrid.DataConnection.SortDescriptions.Clear(); - } + KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift); + + DataGridSortDescription sort = OwningColumn.GetSortDescription(); + IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView; + Debug.Assert(collectionView != null); - // if ctrl is held down, we only clear the sort directions - if (!ctrl) + using (collectionView.DeferRefresh()) { - if (sort != null) + // if shift is held down, we multi-sort, therefore if it isn't, we'll clear the sorts beforehand + if (!shift || owningGrid.DataConnection.SortDescriptions.Count == 0) { - newSort = sort.SwitchSortDirection(); + owningGrid.DataConnection.SortDescriptions.Clear(); + } - // changing direction should not affect sort order, so we replace this column's - // sort description instead of just adding it to the end of the collection - int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort); - if (oldIndex >= 0) + // if ctrl is held down, we only clear the sort directions + if (!ctrl) + { + if (sort != null) { - owningGrid.DataConnection.SortDescriptions.Remove(sort); - owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort); + newSort = sort.SwitchSortDirection(); + + // changing direction should not affect sort order, so we replace this column's + // sort description instead of just adding it to the end of the collection + int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort); + if (oldIndex >= 0) + { + owningGrid.DataConnection.SortDescriptions.Remove(sort); + owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort); + } + else + { + owningGrid.DataConnection.SortDescriptions.Add(newSort); + } } else { + string propertyName = OwningColumn.GetSortPropertyName(); + // no-opt if we couldn't find a property to sort on + if (string.IsNullOrEmpty(propertyName)) + { + return; + } + + newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture); + owningGrid.DataConnection.SortDescriptions.Add(newSort); } } - else - { - string propertyName = OwningColumn.GetSortPropertyName(); - // no-opt if we couldn't find a property to sort on - if (string.IsNullOrEmpty(propertyName)) - { - return; - } - - newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture); - owningGrid.DataConnection.SortDescriptions.Add(newSort); - } } } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs index 5b75bc73f9..46bcd0d347 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs @@ -33,6 +33,11 @@ namespace Avalonia.Controls ColumnReordering?.Invoke(this, e); } + protected internal virtual void OnColumnSorting(DataGridColumnEventArgs e) + { + Sorting?.Invoke(this, e); + } + /// /// Adjusts the widths of all columns with DisplayIndex >= displayIndex such that the total /// width is adjusted by the given amount, if possible. If the total desired adjustment amount diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index d5ce8dba75..c3562c53a4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; @@ -20,6 +21,7 @@ namespace Avalonia.Controls /// /// Represents a row. /// + [PseudoClasses(":selected", ":editing", ":invalid")] public class DataGridRow : TemplatedControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 0833247439..1e03b134b1 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -13,6 +14,7 @@ using System.Reactive.Linq; namespace Avalonia.Controls { + [PseudoClasses(":pressed", ":current", ":expanded")] public class DataGridRowGroupHeader : TemplatedControl { private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton"; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index 8f8b1742ba..0cd3589a57 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Media; using System.Diagnostics; @@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents an individual row header. /// + [PseudoClasses(":invalid", ":selected", ":editing", ":current")] public class DataGridRowHeader : ContentControl { private const string DATAGRIDROWHEADER_elementRootName = "PART_Root"; diff --git a/src/Avalonia.Controls.DataGrid/EventArgs.cs b/src/Avalonia.Controls.DataGrid/EventArgs.cs index 10e2be795e..7590a8ed61 100644 --- a/src/Avalonia.Controls.DataGrid/EventArgs.cs +++ b/src/Avalonia.Controls.DataGrid/EventArgs.cs @@ -289,7 +289,7 @@ namespace Avalonia.Controls /// /// Provides data for column-related events. /// - public class DataGridColumnEventArgs : EventArgs + public class DataGridColumnEventArgs : HandledEventArgs { /// /// Initializes a new instance of the class. @@ -566,4 +566,4 @@ namespace Avalonia.Controls private set; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 11708b360f..af88c569a6 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -12,7 +12,9 @@ MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.Interactivity.RoutedEvent Avalonia.Controls.TreeView.SelectionChangedEvent' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.TreeView.Selection.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.TreeView.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.String[] Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.Args' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.String[] Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.Args.get()' is present in the implementation but not in the contract. MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.DirectProperty Avalonia.Controls.Primitives.SelectingItemsControl.SelectionProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected Avalonia.Controls.ISelectionModel Avalonia.Controls.Primitives.SelectingItemsControl.Selection.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract. -Total Issues: 16 +Total Issues: 18 diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 6dd5b8cc81..e2c8e7e8e2 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -47,6 +47,11 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public event EventHandler Exit; + /// + /// Gets the arguments passed to the AppBuilder Start method. + /// + public string[] Args { get; set; } + /// public ShutdownMode ShutdownMode { get; set; } @@ -68,9 +73,6 @@ namespace Avalonia.Controls.ApplicationLifetimes else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) Shutdown(); } - - - public void Shutdown(int exitCode = 0) { @@ -123,7 +125,11 @@ namespace Avalonia this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) where T : AppBuilderBase, new() { - var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = shutdownMode}; + var lifetime = new ClassicDesktopStyleApplicationLifetime() + { + Args = args, + ShutdownMode = shutdownMode + }; builder.SetupWithLifetime(lifetime); return lifetime.Start(args); } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index a1006d907b..212f0b8617 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -8,6 +8,13 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime { + /// + /// Gets the arguments passed to the + /// + /// method. + /// + string[] Args { get; } + /// /// Gets or sets the . This property indicates whether the application is shutdown explicitly or implicitly. /// If is set to OnExplicitShutdown the application is only closes if Shutdown is called. diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c164f282e8..bfd633c947 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -14,6 +14,7 @@ using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; @@ -30,6 +31,7 @@ namespace Avalonia.Controls /// /// event. /// + [PseudoClasses(":dropdownopen")] public class PopulatedEventArgs : EventArgs { /// @@ -225,6 +227,27 @@ namespace Avalonia.Controls Custom = 13, } + /// + /// Represents the selector used by the + /// control to + /// determine how the specified text should be modified with an item. + /// + /// + /// Modified text that will be used by the + /// . + /// + /// The string used as the basis for filtering. + /// + /// The selected item that should be combined with the + /// parameter. + /// + /// + /// The type used for filtering the + /// . + /// This type can be either a string or an object. + /// + public delegate string AutoCompleteSelector(string search, T item); + /// /// Represents a control that provides a text box for user input and a /// drop-down that contains possible matches based on the input in the text @@ -362,6 +385,9 @@ namespace Avalonia.Controls private AutoCompleteFilterPredicate _itemFilter; private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); + private AutoCompleteSelector _itemSelector; + private AutoCompleteSelector _textSelector; + public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); @@ -528,6 +554,34 @@ namespace Avalonia.Controls (o, v) => o.TextFilter = v, unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> ItemSelectorProperty = + AvaloniaProperty.RegisterDirect>( + nameof(ItemSelector), + o => o.ItemSelector, + (o, v) => o.ItemSelector = v); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> TextSelectorProperty = + AvaloniaProperty.RegisterDirect>( + nameof(TextSelector), + o => o.TextSelector, + (o, v) => o.TextSelector = v); + /// /// Identifies the /// @@ -1061,6 +1115,40 @@ namespace Avalonia.Controls set { SetAndRaise(TextFilterProperty, ref _textFilter, value); } } + /// + /// Gets or sets the custom method that combines the user-entered + /// text and one of the items specified by the + /// . + /// + /// + /// The custom method that combines the user-entered + /// text and one of the items specified by the + /// . + /// + public AutoCompleteSelector ItemSelector + { + get { return _itemSelector; } + set { SetAndRaise(ItemSelectorProperty, ref _itemSelector, value); } + } + + /// + /// Gets or sets the custom method that combines the user-entered + /// text and one of the items specified by the + /// + /// in a text-based way. + /// + /// + /// The custom method that combines the user-entered + /// text and one of the items specified by the + /// + /// in a text-based way. + /// + public AutoCompleteSelector TextSelector + { + get { return _textSelector; } + set { SetAndRaise(TextSelectorProperty, ref _textSelector, value); } + } + public Func>> AsyncPopulator { get { return _asyncPopulator; } @@ -2329,6 +2417,14 @@ namespace Avalonia.Controls { text = SearchText; } + else if (TextSelector != null) + { + text = TextSelector(SearchText, FormatValue(newItem, true)); + } + else if (ItemSelector != null) + { + text = ItemSelector(SearchText, newItem); + } else { text = FormatValue(newItem, true); diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index b54eb2ac57..e94d00b2ff 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Windows.Input; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -28,6 +29,7 @@ namespace Avalonia.Controls /// /// A button control. /// + [PseudoClasses(":pressed")] public class Button : ContentControl { /// diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 44f66d397a..5fe2cf3704 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; @@ -15,6 +16,7 @@ namespace Avalonia.Controls /// /// Represents a spinner control that includes two Buttons. /// + [PseudoClasses(":left", ":right")] public class ButtonSpinner : Spinner { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index 80370df145..76af933b55 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Controls.Metadata; using Avalonia.Input; using System; @@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// Represents a button on a /// . /// + [PseudoClasses(":selected", ":inactive", ":btnfocused")] public sealed class CalendarButton : Button { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index 3a39bd10fa..d5748bb9e4 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -5,10 +5,12 @@ using System; using System.Globalization; +using Avalonia.Controls.Metadata; using Avalonia.Input; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":pressed", ":disabled", ":selected", ":inactive", ":today", ":blackout", ":dayfocused")] public sealed class CalendarDayButton : Button { /// diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 0be7c4f67e..e9ea942142 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -18,6 +19,7 @@ namespace Avalonia.Controls.Primitives /// Represents the currently displayed month or year on a /// . /// + [PseudoClasses(":calendardisabled")] public sealed class CalendarItem : TemplatedControl { /// diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index a86cbc271b..cd60130c5b 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; #nullable enable @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome /// /// Draws window minimize / maximize / close buttons in a when managed client decorations are enabled. /// + [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class CaptionButtons : TemplatedControl { private CompositeDisposable? _disposables; diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index c0c8076dd8..fbddb06952 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; #nullable enable @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome /// /// Draws a titlebar when managed client decorations are enabled. /// + [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class TitleBar : TemplatedControl { private CompositeDisposable? _disposables; diff --git a/src/Avalonia.Controls/ColumnDefinitions.cs b/src/Avalonia.Controls/ColumnDefinitions.cs index ed4f9dbe99..7e355ab357 100644 --- a/src/Avalonia.Controls/ColumnDefinitions.cs +++ b/src/Avalonia.Controls/ColumnDefinitions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Specialized; using System.Linq; +using System.Text; using Avalonia.Collections; namespace Avalonia.Controls @@ -13,7 +14,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public ColumnDefinitions() : base () + public ColumnDefinitions() { } @@ -27,6 +28,11 @@ namespace Avalonia.Controls AddRange(GridLength.ParseLengths(s).Select(x => new ColumnDefinition(x))); } + public override string ToString() + { + return string.Join(",", this.Select(x => x.Width)); + } + /// /// Parses a string representation of column definitions collection. /// @@ -34,4 +40,4 @@ namespace Avalonia.Controls /// The . public static ColumnDefinitions Parse(string s) => new ColumnDefinitions(s); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index dfe9a16532..3c64691816 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -14,6 +15,7 @@ namespace Avalonia.Controls /// /// You will probably only want to create instances inside of control templates. /// + [PseudoClasses(":error")] public class DataValidationErrors : ContentControl { /// diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index a41c159980..8d893154eb 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Interactivity; @@ -11,6 +12,7 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a date /// + [PseudoClasses(":hasnodate")] public class DatePicker : TemplatedControl { /// diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index e54da1fb3a..e4ff5e9e5b 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using System; @@ -9,6 +10,7 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a time /// + [PseudoClasses(":hasnotime")] public class TimePicker : TemplatedControl { /// diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 43882b70c8..9ff2e41fa9 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -1,6 +1,6 @@ using Avalonia.Animation; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Data; namespace Avalonia.Controls { @@ -12,6 +12,7 @@ namespace Avalonia.Controls Right } + [PseudoClasses(":expanded", ":up", ":down", ":left", ":right")] public class Expander : HeaderedContentControl { public static readonly StyledProperty ContentTransitionProperty = diff --git a/src/Avalonia.Controls/IconElement.cs b/src/Avalonia.Controls/IconElement.cs new file mode 100644 index 0000000000..82b7a0660c --- /dev/null +++ b/src/Avalonia.Controls/IconElement.cs @@ -0,0 +1,9 @@ +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls +{ + public abstract class IconElement : TemplatedControl + { + + } +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index a3dfe33641..3aec06e4eb 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Collections; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -18,6 +19,7 @@ namespace Avalonia.Controls /// /// Displays a collection of items. /// + [PseudoClasses(":empty", ":singleitem")] public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener { /// diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index f7e86d697a..d1b8038581 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -163,6 +163,7 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + base.OnApplyTemplate(e); Scroll = e.NameScope.Find("PART_ScrollViewer"); } } diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index e04c79987f..4fe5f4de40 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -1,3 +1,4 @@ +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Input; @@ -6,6 +7,7 @@ namespace Avalonia.Controls /// /// A selectable item in a . /// + [PseudoClasses(":pressed", ":selected")] public class ListBoxItem : ContentControl, ISelectable { /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index b4d3272471..3d8ab3ae48 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reactive.Linq; using System.Windows.Input; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -20,6 +21,7 @@ namespace Avalonia.Controls /// /// A menu item control. /// + [PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")] public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable { /// diff --git a/src/Avalonia.Controls/Mixins/SelectableMixin.cs b/src/Avalonia.Controls/Mixins/SelectableMixin.cs index d2586ab6e8..c9e2b684cb 100644 --- a/src/Avalonia.Controls/Mixins/SelectableMixin.cs +++ b/src/Avalonia.Controls/Mixins/SelectableMixin.cs @@ -48,7 +48,7 @@ namespace Avalonia.Controls.Mixins if (sender != null) { - ((IPseudoClasses)sender.Classes).Set(":selected", (bool)x.NewValue); + ((IPseudoClasses)sender.Classes).Set(":selected", x.NewValue.GetValueOrDefault()); sender.RaiseEvent(new RoutedEventArgs { @@ -58,4 +58,4 @@ namespace Avalonia.Controls.Mixins }); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index 776e9d2171..0349df842b 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -77,7 +77,7 @@ namespace Avalonia.Controls { if (args.Sender is TopLevel tl) { - GetInfo(tl).Exporter?.SetNativeMenu((NativeMenu)args.NewValue); + GetInfo(tl).Exporter?.SetNativeMenu(args.NewValue.GetValueOrDefault()); } }); } diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 4c94d82eb4..a0fec9e677 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls MenuProperty.Changed.Subscribe(args => { var item = (NativeMenuItem)args.Sender; - var value = (NativeMenu)args.NewValue; + var value = args.NewValue.GetValueOrDefault(); if (value.Parent != null && value.Parent != item) throw new InvalidOperationException("NativeMenu already has a parent"); value.Parent = item; diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index f90746bf06..cdbace3ced 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -9,6 +10,7 @@ namespace Avalonia.Controls.Notifications /// /// Control that represents and displays a notification. /// + [PseudoClasses(":error", ":information", ":success", ":warning")] public class NotificationCard : ContentControl { private bool _isClosed; diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 6d9f6b8b77..8f5c6faf40 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -7,12 +7,14 @@ using Avalonia.Controls.Primitives; using Avalonia.Rendering; using Avalonia.Data; using Avalonia.VisualTree; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls.Notifications { /// /// An that displays notifications in a . /// + [PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright")] public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest { private IList _items; diff --git a/src/Avalonia.Controls/PathIcon.cs b/src/Avalonia.Controls/PathIcon.cs new file mode 100644 index 0000000000..764f245249 --- /dev/null +++ b/src/Avalonia.Controls/PathIcon.cs @@ -0,0 +1,21 @@ +using Avalonia.Media; + +namespace Avalonia.Controls +{ + public class PathIcon : IconElement + { + static PathIcon() + { + AffectsRender(DataProperty); + } + + public static readonly StyledProperty DataProperty = + AvaloniaProperty.Register(nameof(Data)); + + public Geometry Data + { + get { return GetValue(DataProperty); } + set { SetValue(DataProperty, value); } + } + } +} diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 37490c3ef3..f5115a2f7c 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -82,7 +82,7 @@ namespace Avalonia.Controls.Presenters TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty, TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty); - Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, + Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, TextAlignmentProperty.Changed, TextWrappingProperty.Changed, TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed, TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed, @@ -282,7 +282,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Constraint = constraint, - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), + Typeface = new Typeface(FontFamily, FontStyle, FontWeight), FontSize = FontSize, Text = text ?? string.Empty, TextAlignment = TextAlignment, @@ -499,7 +499,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Text = "X", - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), + Typeface = new Typeface(FontFamily, FontStyle, FontWeight), FontSize = FontSize, TextAlignment = TextAlignment, Constraint = availableSize, diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 89f672deaa..7a5e6ce426 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -126,7 +126,7 @@ namespace Avalonia.Controls.Primitives if (shapedTextCharacters.GlyphRun.Characters.End < textPosition) { - currentX += shapedTextCharacters.GlyphRun.Bounds.Width; + currentX += shapedTextCharacters.Size.Width; continue; } @@ -143,7 +143,7 @@ namespace Avalonia.Controls.Primitives width = 0.0; } - return new Rect(currentX, currentY, width, shapedTextCharacters.GlyphRun.Bounds.Height); + return new Rect(currentX, currentY, width, shapedTextCharacters.Size.Height); } } diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs index e424bf683d..82a49c4189 100644 --- a/src/Avalonia.Controls/Primitives/IPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Input; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -13,7 +14,7 @@ namespace Avalonia.Controls.Primitives /// () or an which is created /// on an . /// - public interface IPopupHost : IDisposable + public interface IPopupHost : IDisposable, IFocusScope { /// /// Sets the control to display in the popup. diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index 8c464c7aad..7f1dbdf592 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -221,7 +221,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning if (!FitsInBounds(unconstrainedRect, PopupAnchor.Bottom)) { - unconstrainedRect = unconstrainedRect.WithHeight(bounds.Height - unconstrainedRect.Y); + unconstrainedRect = unconstrainedRect.WithHeight(bounds.Bottom - unconstrainedRect.Y); } if (IsValid(unconstrainedRect)) diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index fc82fcc7a7..a7fb7ae08c 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -4,6 +4,7 @@ using Avalonia.Interactivity; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Threading; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls.Primitives { @@ -21,6 +22,7 @@ namespace Avalonia.Controls.Primitives /// /// A scrollbar control. /// + [PseudoClasses(":vertical", ":horizontal")] public class ScrollBar : RangeBase { /// @@ -141,7 +143,7 @@ namespace Avalonia.Controls.Primitives _ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.") }; - SetValue(IsVisibleProperty, isVisible, BindingPriority.Style); + SetValue(IsVisibleProperty, isVisible); } protected override void OnKeyDown(KeyEventArgs e) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 5f8c5da2f8..e34b3b145f 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -6,7 +6,6 @@ using System.ComponentModel; using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Selection; -using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; @@ -70,8 +69,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - protected static readonly DirectProperty SelectedItemsProperty = - AvaloniaProperty.RegisterDirect( + protected static readonly DirectProperty SelectedItemsProperty = + AvaloniaProperty.RegisterDirect( nameof(SelectedItems), o => o.SelectedItems, (o, v) => o.SelectedItems = v); @@ -111,12 +110,13 @@ namespace Avalonia.Controls.Primitives RoutingStrategies.Bubble); private static readonly IList Empty = Array.Empty(); - private SelectedItemsSync? _selectedItemsSync; private ISelectionModel? _selection; private int _oldSelectedIndex; private object? _oldSelectedItem; - private int _initializing; + private IList? _oldSelectedItems; private bool _ignoreContainerSelectionChanged; + private UpdateState? _updateState; + private bool _hasScrolledToSelectedItem; /// /// Initializes static members of the class. @@ -149,8 +149,27 @@ namespace Avalonia.Controls.Primitives /// public int SelectedIndex { - get => Selection.SelectedIndex; - set => Selection.SelectedIndex = value; + get + { + // When a Begin/EndInit/DataContext update is in place we return the value to be + // updated here, even though it's not yet active and the property changed notification + // has not yet been raised. If we don't do this then the old value will be written back + // to the source when two-way bound, and the update value will be lost. + return _updateState?.SelectedIndex.HasValue == true ? + _updateState.SelectedIndex.Value : + Selection.SelectedIndex; + } + set + { + if (_updateState is object) + { + _updateState.SelectedIndex = value; + } + else + { + Selection.SelectedIndex = value; + } + } } /// @@ -158,17 +177,67 @@ namespace Avalonia.Controls.Primitives /// public object? SelectedItem { - get => Selection.SelectedItem; - set => Selection.SelectedItem = value; + get + { + // See SelectedIndex setter for more information. + return _updateState?.SelectedItem.HasValue == true ? + _updateState.SelectedItem.Value : + Selection.SelectedItem; + } + set + { + if (_updateState is object) + { + _updateState.SelectedItem = value; + } + else + { + Selection.SelectedItem = value; + } + } } /// /// Gets or sets the selected items. /// - protected IList SelectedItems + /// + /// By default returns a collection that can be modified in order to manipulate the control + /// selection, however this property will return null if is + /// re-assigned; you should only use _either_ Selection or SelectedItems. + /// + protected IList? SelectedItems { - get => SelectedItemsSync.SelectedItems; - set => SelectedItemsSync.SelectedItems = value; + get + { + // See SelectedIndex setter for more information. + if (_updateState?.SelectedItems.HasValue == true) + { + return _updateState.SelectedItems.Value; + } + else if (Selection is InternalSelectionModel ism) + { + var result = ism.WritableSelectedItems; + _oldSelectedItems = result; + return result; + } + + return null; + } + set + { + if (_updateState is object) + { + _updateState.SelectedItems = new Optional(value); + } + else if (Selection is InternalSelectionModel i) + { + i.WritableSelectedItems = value; + } + else + { + throw new InvalidOperationException("Cannot set both Selection and SelectedItems."); + } + } } /// @@ -178,19 +247,30 @@ namespace Avalonia.Controls.Primitives { get { - if (_selection is null) + if (_updateState?.Selection.HasValue == true) { - _selection = CreateDefaultSelectionModel(); - InitializeSelectionModel(_selection); + return _updateState.Selection.Value; } + else + { + if (_selection is null) + { + _selection = CreateDefaultSelectionModel(); + InitializeSelectionModel(_selection); + } - return _selection; + return _selection; + } } set { value ??= CreateDefaultSelectionModel(); - if (_selection != value) + if (_updateState is object) + { + _updateState.Selection = new Optional(value); + } + else if (_selection != value) { if (value.Source != null && value.Source != Items) { @@ -212,6 +292,15 @@ namespace Avalonia.Controls.Primitives } InitializeSelectionModel(_selection); + + if (_oldSelectedItems != SelectedItems) + { + RaisePropertyChanged( + SelectedItemsProperty, + new Optional(_oldSelectedItems), + new BindingValue(SelectedItems)); + _oldSelectedItems = SelectedItems; + } } } } @@ -234,20 +323,18 @@ namespace Avalonia.Controls.Primitives /// protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0; - private SelectedItemsSync SelectedItemsSync => _selectedItemsSync ??= new SelectedItemsSync(Selection); - /// public override void BeginInit() { base.BeginInit(); - ++_initializing; + BeginUpdating(); } /// public override void EndInit() { base.EndInit(); - --_initializing; + EndUpdating(); } /// @@ -295,6 +382,28 @@ namespace Avalonia.Controls.Primitives } } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + AutoScrollToSelectedItemIfNecessary(); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + void ExecuteScrollWhenLayoutUpdated(object sender, EventArgs e) + { + LayoutUpdated -= ExecuteScrollWhenLayoutUpdated; + AutoScrollToSelectedItemIfNecessary(); + } + + if (AutoScrollToSelectedItem) + { + LayoutUpdated += ExecuteScrollWhenLayoutUpdated; + } + } + /// protected override void OnContainersMaterialized(ItemContainerEventArgs e) { @@ -351,30 +460,14 @@ namespace Avalonia.Controls.Primitives protected override void OnDataContextBeginUpdate() { base.OnDataContextBeginUpdate(); - ++_initializing; - - if (_selection is object) - { - _selection.Source = null; - } + BeginUpdating(); } /// protected override void OnDataContextEndUpdate() { base.OnDataContextEndUpdate(); - --_initializing; - - if (_selection is object && _initializing == 0) - { - _selection.Source = Items; - - if (Items is null) - { - _selection.Clear(); - _selectedItemsSync?.SelectedItems?.Clear(); - } - } + EndUpdating(); } protected override void OnInitialized() @@ -398,8 +491,7 @@ namespace Avalonia.Controls.Primitives if (ItemCount > 0 && Match(keymap.SelectAll) && - (((SelectionMode & SelectionMode.Multiple) != 0) || - (SelectionMode & SelectionMode.Toggle) != 0)) + SelectionMode.HasFlag(SelectionMode.Multiple)) { Selection.SelectAll(); e.Handled = true; @@ -411,9 +503,11 @@ namespace Avalonia.Controls.Primitives { base.OnPropertyChanged(change); - if (change.Property == ItemsProperty && - _initializing == 0 && - _selection is object) + if (change.Property == AutoScrollToSelectedItemProperty) + { + AutoScrollToSelectedItemIfNecessary(); + } + if (change.Property == ItemsProperty && _updateState is null && _selection is object) { var newValue = change.NewValue.GetValueOrDefault(); _selection.Source = newValue; @@ -601,23 +695,30 @@ namespace Avalonia.Controls.Primitives /// The event args. private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(ISelectionModel.AnchorIndex) && AutoScrollToSelectedItem) + if (e.PropertyName == nameof(ISelectionModel.AnchorIndex)) { - if (Selection.AnchorIndex > 0) - { - ScrollIntoView(Selection.AnchorIndex); - } + _hasScrolledToSelectedItem = false; + AutoScrollToSelectedItemIfNecessary(); } - else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex)) + else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex) { RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex); _oldSelectedIndex = SelectedIndex; } - else if (e.PropertyName == nameof(ISelectionModel.SelectedItem)) + else if (e.PropertyName == nameof(ISelectionModel.SelectedItem) && _oldSelectedItem != SelectedItem) { RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem); _oldSelectedItem = SelectedItem; } + else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) && + _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) + { + RaisePropertyChanged( + SelectedItemsProperty, + new Optional(_oldSelectedItems), + new BindingValue(SelectedItems)); + _oldSelectedItems = SelectedItems; + } } /// @@ -674,6 +775,19 @@ namespace Avalonia.Controls.Primitives } } + private void AutoScrollToSelectedItemIfNecessary() + { + if (AutoScrollToSelectedItem && + !_hasScrolledToSelectedItem && + Presenter is object && + Selection.AnchorIndex >= 0 && + ((IVisual)this).IsAttachedToVisualTree) + { + ScrollIntoView(Selection.AnchorIndex); + _hasScrolledToSelectedItem = true; + } + } + /// /// Called when a container raises the . /// @@ -734,14 +848,6 @@ namespace Avalonia.Controls.Primitives } } - private void MarkContainersUnselected() - { - foreach (var container in ItemContainerGenerator.Containers) - { - MarkContainerSelected(container.ContainerControl, false); - } - } - /// /// Sets an item container's 'selected' class or . /// @@ -757,23 +863,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Sets an item container's 'selected' class or . - /// - /// The item. - /// Whether the item should be selected or deselected. - private int MarkItemSelected(object item, bool selected) - { - var index = IndexOf(Items, item); - - if (index != -1) - { - MarkItemSelected(index, selected); - } - - return index; - } - private void UpdateContainerSelection() { if (Presenter?.Panel is IPanel panel) @@ -789,7 +878,7 @@ namespace Avalonia.Controls.Primitives private ISelectionModel CreateDefaultSelectionModel() { - return new SelectionModel + return new InternalSelectionModel { SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), }; @@ -797,7 +886,7 @@ namespace Avalonia.Controls.Primitives private void InitializeSelectionModel(ISelectionModel model) { - if (_initializing == 0) + if (_updateState is null) { model.Source = Items; } @@ -825,9 +914,6 @@ namespace Avalonia.Controls.Primitives UpdateContainerSelection(); - _selectedItemsSync ??= new SelectedItemsSync(model); - _selectedItemsSync.SelectionModel = model; - if (SelectedIndex != -1) { RaiseEvent(new SelectionChangedEventArgs( @@ -845,5 +931,96 @@ namespace Avalonia.Controls.Primitives model.SelectionChanged -= OnSelectionModelSelectionChanged; } } + + private void BeginUpdating() + { + _updateState ??= new UpdateState(); + _updateState.UpdateCount++; + } + + private void EndUpdating() + { + if (_updateState is object && --_updateState.UpdateCount == 0) + { + var state = _updateState; + _updateState = null; + + if (state.Selection.HasValue) + { + Selection = state.Selection.Value; + } + + if (state.SelectedItems.HasValue) + { + SelectedItems = state.SelectedItems.Value; + } + + Selection.Source = Items; + + if (Items is null) + { + Selection.Clear(); + } + + if (state.SelectedIndex.HasValue) + { + SelectedIndex = state.SelectedIndex.Value; + } + else if (state.SelectedItem.HasValue) + { + SelectedItem = state.SelectedItem.Value; + } + } + } + + // When in a BeginInit..EndInit block, or when the DataContext is updating, we need to + // defer changes to the selection model because we have no idea in which order properties + // will be set. Consider: + // + // - Both Items and SelectedItem are bound + // - The DataContext changes + // - The binding for SelectedItem updates first, producing an item + // - Items is searched to find the index of the new selected item + // - However Items isn't yet updated; the item is not found + // - SelectedIndex is incorrectly set to -1 + // + // This logic cannot be encapsulated in SelectionModel because the selection model can also + // be bound, consider: + // + // - Both Items and Selection are bound + // - The DataContext changes + // - The binding for Items updates first + // - The new items are assigned to Selection.Source + // - The binding for Selection updates, producing a new SelectionModel + // - Both the old and new SelectionModels have the incorrect Source + private class UpdateState + { + private Optional _selectedIndex; + private Optional _selectedItem; + + public int UpdateCount { get; set; } + public Optional Selection { get; set; } + public Optional SelectedItems { get; set; } + + public Optional SelectedIndex + { + get => _selectedIndex; + set + { + _selectedIndex = value; + _selectedItem = default; + } + } + + public Optional SelectedItem + { + get => _selectedItem; + set + { + _selectedItem = value; + _selectedIndex = default; + } + } + } } } diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 96810ed01b..348922b71d 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -1,9 +1,11 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Interactivity; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":pressed")] public class Thumb : TemplatedControl { public static readonly RoutedEvent DragStartedEvent = diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 13031ddad8..f96ca9310d 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Interactivity; @@ -7,6 +8,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states. /// + [PseudoClasses(":checked", ":unchecked", ":indeterminate")] public class ToggleButton : Button { /// diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 29e7f28b44..9399f5fb31 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -4,6 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; @@ -12,6 +13,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { + [PseudoClasses(":vertical", ":horizontal")] public class Track : Control { public static readonly DirectProperty MinimumProperty = diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index a92f24a050..161f09d9b6 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Layout; using Avalonia.Media; @@ -8,6 +9,7 @@ namespace Avalonia.Controls /// /// A control used to indicate the progress of an operation. /// + [PseudoClasses(":vertical", ":horizontal", ":indeterminate")] public class ProgressBar : RangeBase { public class ProgressBarTemplateProperties : AvaloniaObject diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 8bc356bdec..40f1b8dbb9 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -7,10 +7,10 @@ using System; using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Templates; -using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -681,8 +681,15 @@ namespace Avalonia.Controls if (oldValue != null) { oldValue.UninitializeForContext(LayoutContext); - oldValue.MeasureInvalidated -= InvalidateMeasureForLayout; - oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout; + + WeakEventHandlerManager.Unsubscribe( + oldValue, + nameof(AttachedLayout.MeasureInvalidated), + InvalidateMeasureForLayout); + WeakEventHandlerManager.Unsubscribe( + oldValue, + nameof(AttachedLayout.ArrangeInvalidated), + InvalidateArrangeForLayout); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -699,8 +706,15 @@ namespace Avalonia.Controls if (newValue != null) { newValue.InitializeForContext(LayoutContext); - newValue.MeasureInvalidated += InvalidateMeasureForLayout; - newValue.ArrangeInvalidated += InvalidateArrangeForLayout; + + WeakEventHandlerManager.Subscribe( + newValue, + nameof(AttachedLayout.MeasureInvalidated), + InvalidateMeasureForLayout); + WeakEventHandlerManager.Subscribe( + newValue, + nameof(AttachedLayout.ArrangeInvalidated), + InvalidateArrangeForLayout); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 4600301410..6b75149d62 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -448,6 +448,38 @@ namespace Avalonia.Controls Offset += new Vector(_smallChange.Width, 0); } + /// + /// Scrolls the content upward by one page. + /// + public void PageUp() + { + VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0); + } + + /// + /// Scrolls the content downward by one page. + /// + public void PageDown() + { + VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum); + } + + /// + /// Scrolls the content left by one page. + /// + public void PageLeft() + { + HorizontalScrollBarValue = Math.Max(_offset.X - _viewport.Width, 0); + } + + /// + /// Scrolls the content tight by one page. + /// + public void PageRight() + { + HorizontalScrollBarValue = Math.Min(_offset.X + _viewport.Width, HorizontalScrollBarMaximum); + } + /// /// Scrolls to the top-left corner of the content. /// @@ -623,12 +655,12 @@ namespace Avalonia.Controls { if (e.Key == Key.PageUp) { - VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0); + PageUp(); e.Handled = true; } else if (e.Key == Key.PageDown) { - VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum); + PageDown(); e.Handled = true; } } diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs new file mode 100644 index 0000000000..fcdaf44166 --- /dev/null +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Avalonia.Collections; + +#nullable enable + +namespace Avalonia.Controls.Selection +{ + internal class InternalSelectionModel : SelectionModel + { + private IList? _writableSelectedItems; + private bool _ignoreModelChanges; + private bool _ignoreSelectedItemsChanges; + + public InternalSelectionModel() + { + SelectionChanged += OnSelectionChanged; + SourceReset += OnSourceReset; + } + + [AllowNull] + public IList WritableSelectedItems + { + get + { + if (_writableSelectedItems is null) + { + _writableSelectedItems = new AvaloniaList(); + SubscribeToSelectedItems(); + } + + return _writableSelectedItems; + } + set + { + value ??= new AvaloniaList(); + + if (value.IsFixedSize) + { + throw new NotSupportedException("Cannot assign fixed size selection to SelectedItems."); + } + + if (_writableSelectedItems != value) + { + UnsubscribeFromSelectedItems(); + _writableSelectedItems = value; + SyncFromSelectedItems(); + SubscribeToSelectedItems(); + + if (ItemsView is null) + { + SetInitSelectedItems(value); + } + + RaisePropertyChanged(nameof(WritableSelectedItems)); + } + } + } + + private protected override void SetSource(IEnumerable? value) + { + if (Source == value) + { + return; + } + + object?[]? oldSelection = null; + + if (Source is object && value is object) + { + oldSelection = new object?[WritableSelectedItems.Count]; + WritableSelectedItems.CopyTo(oldSelection, 0); + } + + try + { + _ignoreSelectedItemsChanges = true; + base.SetSource(value); + } + finally + { + _ignoreSelectedItemsChanges = false; + } + + if (oldSelection is null) + { + SyncToSelectedItems(); + } + else + { + foreach (var i in oldSelection) + { + var index = ItemsView!.IndexOf(i); + Select(index); + } + } + } + + private void SyncToSelectedItems() + { + if (_writableSelectedItems is object) + { + try + { + _ignoreSelectedItemsChanges = true; + _writableSelectedItems.Clear(); + + foreach (var i in base.SelectedItems) + { + _writableSelectedItems.Add(i); + } + } + finally + { + _ignoreSelectedItemsChanges = false; + } + } + } + + private void SyncFromSelectedItems() + { + if (Source is null || _writableSelectedItems is null) + { + return; + } + + try + { + _ignoreModelChanges = true; + + using (BatchUpdate()) + { + Clear(); + Add(_writableSelectedItems); + } + } + finally + { + _ignoreModelChanges = false; + } + } + + private void SubscribeToSelectedItems() + { + if (_writableSelectedItems is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnSelectedItemsCollectionChanged; + } + } + + private void UnsubscribeFromSelectedItems() + { + if (_writableSelectedItems is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnSelectedItemsCollectionChanged; + } + } + + private void OnSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) + { + if (_ignoreModelChanges) + { + return; + } + + try + { + var items = WritableSelectedItems; + var deselected = e.DeselectedItems.ToList(); + var selected = e.SelectedItems.ToList(); + + _ignoreSelectedItemsChanges = true; + + foreach (var i in deselected) + { + items.Remove(i); + } + + foreach (var i in selected) + { + items.Add(i); + } + } + finally + { + _ignoreSelectedItemsChanges = false; + } + } + + private void OnSourceReset(object sender, EventArgs e) => SyncFromSelectedItems(); + + private void OnSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (_ignoreSelectedItemsChanges) + { + return; + } + + if (_writableSelectedItems == null) + { + throw new AvaloniaInternalException("CollectionChanged raised but we don't have items."); + } + + void Remove() + { + foreach (var i in e.OldItems) + { + var index = IndexOf(Source, i); + + if (index != -1) + { + Deselect(index); + } + } + } + + try + { + using var operation = BatchUpdate(); + + _ignoreModelChanges = true; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + Remove(); + break; + case NotifyCollectionChangedAction.Replace: + Remove(); + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Reset: + Clear(); + Add(_writableSelectedItems); + break; + } + } + finally + { + _ignoreModelChanges = false; + } + } + + private void Add(IList newItems) + { + foreach (var i in newItems) + { + var index = IndexOf(Source, i); + + if (index != -1) + { + Select(index); + } + } + } + + private static int IndexOf(object? source, object? item) + { + if (source is IList l) + { + return l.IndexOf(item); + } + else if (source is ItemsSourceView v) + { + return v.IndexOf(item); + } + + return -1; + } + } +} diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 2556bd4c4c..3b5d57a7b8 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -20,8 +20,7 @@ namespace Avalonia.Controls.Selection private SelectedItems? _selectedItems; private SelectedItems.Untyped? _selectedItemsUntyped; private EventHandler? _untypedSelectionChanged; - [AllowNull] private T _initSelectedItem = default; - private bool _hasInitSelectedItem; + private IList? _initSelectedItems; public SelectionModel() { @@ -82,7 +81,19 @@ namespace Avalonia.Controls.Selection [MaybeNull, AllowNull] public T SelectedItem { - get => ItemsView is object ? GetItemAt(_selectedIndex) : _initSelectedItem; + get + { + if (ItemsView is object) + { + return GetItemAt(_selectedIndex); + } + else if (_initSelectedItems is object && _initSelectedItems.Count > 0) + { + return (T)_initSelectedItems[0]; + } + + return default; + } set { if (ItemsView is object) @@ -92,8 +103,9 @@ namespace Avalonia.Controls.Selection else { Clear(); - _initSelectedItem = value; - _hasInitSelectedItem = true; +#pragma warning disable CS8601 + SetInitSelectedItems(new T[] { value }); +#pragma warning restore CS8601 } } } @@ -102,9 +114,10 @@ namespace Avalonia.Controls.Selection { get { - if (ItemsView is null && _hasInitSelectedItem) + if (ItemsView is null && _initSelectedItems is object) { - return new[] { _initSelectedItem }; + return _initSelectedItems is IReadOnlyList i ? + i : _initSelectedItems.Cast().ToList(); } return _selectedItems ??= new SelectedItems(this); @@ -229,12 +242,7 @@ namespace Avalonia.Controls.Selection { using var update = BatchUpdate(); var o = update.Operation; - var range = CoerceRange(start, end); - - if (range.Begin == -1) - { - return; - } + var range = new IndexRange(Math.Max(0, start), end); if (RangesEnabled) { @@ -258,8 +266,7 @@ namespace Avalonia.Controls.Selection o.SelectedIndex = -1; } - _initSelectedItem = default; - _hasInitSelectedItem = false; + _initSelectedItems = null; } public void SelectAll() => SelectRange(0, int.MaxValue); @@ -270,7 +277,7 @@ namespace Avalonia.Controls.Selection PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - private void SetSource(IEnumerable? value) + private protected virtual void SetSource(IEnumerable? value) { if (base.Source != value) { @@ -292,11 +299,14 @@ namespace Avalonia.Controls.Selection { update.Operation.IsSourceUpdate = true; - if (_hasInitSelectedItem) + if (_initSelectedItems is object && ItemsView is object) { - SelectedItem = _initSelectedItem; - _initSelectedItem = default; - _hasInitSelectedItem = false; + foreach (T i in _initSelectedItems) + { + Select(ItemsView.IndexOf(i)); + } + + _initSelectedItems = null; } else { @@ -345,7 +355,9 @@ namespace Avalonia.Controls.Selection LostSelection(this, EventArgs.Empty); } - CommitOperation(update.Operation); + // Don't raise PropertyChanged events here as the OnSourceCollectionChanged event that + // let to this method being called will raise them if necessary. + CommitOperation(update.Operation, raisePropertyChanged: false); } private protected override CollectionChangeState OnItemsAdded(int index, IList items) @@ -430,6 +442,11 @@ namespace Avalonia.Controls.Selection RaisePropertyChanged(nameof(SelectedIndex)); } + if (e.Action == NotifyCollectionChangedAction.Remove && e.OldStartingIndex <= oldSelectedIndex) + { + RaisePropertyChanged(nameof(SelectedItem)); + } + if (oldAnchorIndex != _anchorIndex) { RaisePropertyChanged(nameof(AnchorIndex)); @@ -459,6 +476,16 @@ namespace Avalonia.Controls.Selection return true; } + private protected void SetInitSelectedItems(IList items) + { + if (Source is object) + { + throw new InvalidOperationException("Cannot set init selected items when Source is set."); + } + + _initSelectedItems = items; + } + protected override void OnSourceCollectionChangeFinished() { if (_operation is object) @@ -532,8 +559,7 @@ namespace Avalonia.Controls.Selection o.SelectedIndex = o.AnchorIndex = start; } - _initSelectedItem = default; - _hasInitSelectedItem = false; + _initSelectedItems = null; } [return: MaybeNull] @@ -611,7 +637,7 @@ namespace Avalonia.Controls.Selection } } - private void CommitOperation(Operation operation) + private void CommitOperation(Operation operation, bool raisePropertyChanged = true) { try { @@ -679,23 +705,34 @@ namespace Avalonia.Controls.Selection } } - if (oldSelectedIndex != _selectedIndex) + if (raisePropertyChanged) { - indexesChanged = true; - RaisePropertyChanged(nameof(SelectedIndex)); - RaisePropertyChanged(nameof(SelectedItem)); - } + if (oldSelectedIndex != _selectedIndex) + { + indexesChanged = true; + RaisePropertyChanged(nameof(SelectedIndex)); + } - if (oldAnchorIndex != _anchorIndex) - { - indexesChanged = true; - RaisePropertyChanged(nameof(AnchorIndex)); - } + if (oldSelectedIndex != _selectedIndex || operation.IsSourceUpdate) + { + RaisePropertyChanged(nameof(SelectedItem)); + } - if (indexesChanged) - { - RaisePropertyChanged(nameof(SelectedIndexes)); - RaisePropertyChanged(nameof(SelectedItems)); + if (oldAnchorIndex != _anchorIndex) + { + indexesChanged = true; + RaisePropertyChanged(nameof(AnchorIndex)); + } + + if (indexesChanged) + { + RaisePropertyChanged(nameof(SelectedIndexes)); + } + + if (indexesChanged || operation.IsSourceUpdate) + { + RaisePropertyChanged(nameof(SelectedItems)); + } } } finally diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 293cbac82f..6e08e78813 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -39,6 +40,7 @@ namespace Avalonia.Controls /// /// A control that lets the user select from a range of values by moving a Thumb control along a Track. /// + [PseudoClasses(":vertical", ":horizontal", ":pressed")] public class Slider : RangeBase { /// diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index b71858f796..8267efc466 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Primitives; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; @@ -73,6 +74,10 @@ namespace Avalonia.Controls /// /// A control with two views: A collapsible pane and an area for content /// + [PseudoClasses(":open", ":closed")] + [PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline")] + [PseudoClasses(":left", ":right")] + [PseudoClasses(":lightdismiss")] public class SplitView : TemplatedControl { /* diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index f81e355a7d..306a9d3e6a 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; @@ -66,7 +67,7 @@ namespace Avalonia.Controls SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); - SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); + SelectedItemProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent()); } /// @@ -145,55 +146,27 @@ namespace Avalonia.Controls protected override void OnContainersMaterialized(ItemContainerEventArgs e) { base.OnContainersMaterialized(e); - - if (SelectedContent != null || SelectedIndex == -1) - { - return; - } - - var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); - - if (container == null) - { - return; - } - - UpdateSelectedContent(container); + UpdateSelectedContent(); } - private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) + protected override void OnContainersRecycled(ItemContainerEventArgs e) { - var index = (int)e.NewValue; - - if (index == -1) - { - SelectedContentTemplate = null; - - SelectedContent = null; - - return; - } - - var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index); - - if (container == null) - { - return; - } - - UpdateSelectedContent(container); + base.OnContainersRecycled(e); + UpdateSelectedContent(); } - private void UpdateSelectedContent(IContentControl item) + private void UpdateSelectedContent() { - if (SelectedContentTemplate != item.ContentTemplate) + if (SelectedIndex == -1) { - SelectedContentTemplate = item.ContentTemplate; + SelectedContent = SelectedContentTemplate = null; } - - if (SelectedContent != item.Content) + else { - SelectedContent = item.Content; + var container = SelectedItem as IContentControl ?? + ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as IContentControl; + SelectedContentTemplate = container?.ContentTemplate; + SelectedContent = container?.Content; } } diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 6320443a13..593643a1eb 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -1,3 +1,4 @@ +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; @@ -6,6 +7,7 @@ namespace Avalonia.Controls /// /// An item in a or . /// + [PseudoClasses(":pressed", ":selected")] public class TabItem : HeaderedContentControl, ISelectable { /// diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 7e5287f81f..d61519e697 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -138,7 +138,7 @@ namespace Avalonia.Controls FontStyleProperty, TextWrappingProperty, FontFamilyProperty, TextTrimmingProperty, TextProperty, PaddingProperty, LineHeightProperty, MaxLinesProperty); - Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed, + Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed, TextAlignmentProperty.Changed, TextWrappingProperty.Changed, TextTrimmingProperty.Changed, FontSizeProperty.Changed, FontStyleProperty.Changed, FontWeightProperty.Changed, @@ -434,7 +434,10 @@ namespace Avalonia.Controls var padding = Padding; - TextLayout.Draw(context, new Point(padding.Left + offsetX, padding.Top)); + using (context.PushPostTransform(Matrix.CreateTranslation(padding.Left + offsetX, padding.Top))) + { + TextLayout.Draw(context); + } } /// @@ -452,7 +455,7 @@ namespace Avalonia.Controls return new TextLayout( text ?? string.Empty, - FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), + new Typeface(FontFamily, FontStyle, FontWeight), FontSize, Foreground, TextAlignment, diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 73a1ae3335..0fe3ac62e4 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -13,9 +13,11 @@ using Avalonia.Metadata; using Avalonia.Data; using Avalonia.Layout; using Avalonia.Utilities; +using Avalonia.Controls.Metadata; namespace Avalonia.Controls { + [PseudoClasses(":empty")] public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost { public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index c32f2d8102..662a355dac 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Presenters; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; @@ -8,6 +9,7 @@ namespace Avalonia.Controls /// /// A Toggle Switch control. /// + [PseudoClasses(":dragging")] public class ToggleSwitch : ToggleButton { private Panel _knobsPanel; diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index d56ff5752f..71bd0726d4 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Linq; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.VisualTree; @@ -14,6 +15,7 @@ namespace Avalonia.Controls /// To add a tooltip to a control, use the attached property, /// assigning the content that you want displayed. /// + [PseudoClasses(":open")] public class ToolTip : ContentControl { /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 4942d4d313..8ce258b546 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -1,5 +1,6 @@ using System.Linq; using Avalonia.Controls.Generators; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -11,6 +12,7 @@ namespace Avalonia.Controls /// /// An item in a . /// + [PseudoClasses(":pressed", ":selected")] public class TreeViewItem : HeaderedItemsControl, ISelectable { /// diff --git a/src/Avalonia.Controls/Utils/SelectedItemsSync.cs b/src/Avalonia.Controls/Utils/SelectedItemsSync.cs deleted file mode 100644 index 83b62c7b6e..0000000000 --- a/src/Avalonia.Controls/Utils/SelectedItemsSync.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Linq; -using Avalonia.Collections; -using Avalonia.Controls.Selection; - -#nullable enable - -namespace Avalonia.Controls.Utils -{ - /// - /// Synchronizes an with a list of SelectedItems. - /// - internal class SelectedItemsSync : IDisposable - { - private ISelectionModel _selectionModel; - private IList _selectedItems; - private bool _updatingItems; - private bool _updatingModel; - - public SelectedItemsSync(ISelectionModel model) - { - _selectionModel = model ?? throw new ArgumentNullException(nameof(model)); - _selectedItems = new AvaloniaList(); - SyncSelectedItemsWithSelectionModel(); - SubscribeToSelectedItems(_selectedItems); - SubscribeToSelectionModel(model); - } - - public ISelectionModel SelectionModel - { - get => _selectionModel; - set - { - if (_selectionModel != value) - { - value = value ?? throw new ArgumentNullException(nameof(value)); - UnsubscribeFromSelectionModel(_selectionModel); - _selectionModel = value; - SubscribeToSelectionModel(_selectionModel); - SyncSelectedItemsWithSelectionModel(); - } - } - } - - public IList SelectedItems - { - get => _selectedItems; - set - { - value ??= new AvaloniaList(); - - if (_selectedItems != value) - { - if (value.IsFixedSize) - { - throw new NotSupportedException( - "Cannot assign fixed size selection to SelectedItems."); - } - - UnsubscribeFromSelectedItems(_selectedItems); - _selectedItems = value; - SubscribeToSelectedItems(_selectedItems); - SyncSelectionModelWithSelectedItems(); - } - } - } - - public void Dispose() - { - UnsubscribeFromSelectedItems(_selectedItems); - UnsubscribeFromSelectionModel(_selectionModel); - } - - private void SyncSelectedItemsWithSelectionModel() - { - _updatingItems = true; - - try - { - _selectedItems.Clear(); - - if (_selectionModel.Source is object) - { - foreach (var i in _selectionModel.SelectedItems) - { - _selectedItems.Add(i); - } - } - } - finally - { - _updatingItems = false; - } - } - - private void SyncSelectionModelWithSelectedItems() - { - _updatingModel = true; - - try - { - if (_selectionModel.Source is object) - { - using (_selectionModel.BatchUpdate()) - { - SelectionModel.Clear(); - Add(_selectedItems); - } - } - } - finally - { - _updatingModel = false; - } - } - - private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (_updatingItems) - { - return; - } - - if (_selectedItems == null) - { - throw new AvaloniaInternalException("CollectionChanged raised but we don't have items."); - } - - void Remove() - { - foreach (var i in e.OldItems) - { - var index = IndexOf(SelectionModel.Source, i); - - if (index != -1) - { - SelectionModel.Deselect(index); - } - } - } - - try - { - using var operation = SelectionModel.BatchUpdate(); - - _updatingModel = true; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - Add(e.NewItems); - break; - case NotifyCollectionChangedAction.Remove: - Remove(); - break; - case NotifyCollectionChangedAction.Replace: - Remove(); - Add(e.NewItems); - break; - case NotifyCollectionChangedAction.Reset: - SelectionModel.Clear(); - Add(_selectedItems); - break; - } - } - finally - { - _updatingModel = false; - } - } - - private void Add(IList newItems) - { - foreach (var i in newItems) - { - var index = IndexOf(SelectionModel.Source, i); - - if (index != -1) - { - SelectionModel.Select(index); - } - } - } - - private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(ISelectionModel.Source)) - { - if (_selectedItems.Count > 0) - { - SyncSelectionModelWithSelectedItems(); - } - else - { - SyncSelectedItemsWithSelectionModel(); - } - } - } - - private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) - { - if (_updatingModel || _selectionModel.Source is null) - { - return; - } - - try - { - var deselected = e.DeselectedItems.ToList(); - var selected = e.SelectedItems.ToList(); - - _updatingItems = true; - - foreach (var i in deselected) - { - _selectedItems.Remove(i); - } - - foreach (var i in selected) - { - _selectedItems.Add(i); - } - } - finally - { - _updatingItems = false; - } - } - - private void SelectionModelSourceReset(object sender, EventArgs e) - { - SyncSelectionModelWithSelectedItems(); - } - - - private void SubscribeToSelectedItems(IList selectedItems) - { - if (selectedItems is INotifyCollectionChanged incc) - { - incc.CollectionChanged += SelectedItemsCollectionChanged; - } - } - - private void SubscribeToSelectionModel(ISelectionModel model) - { - model.PropertyChanged += SelectionModelPropertyChanged; - model.SelectionChanged += SelectionModelSelectionChanged; - model.SourceReset += SelectionModelSourceReset; - } - - private void UnsubscribeFromSelectedItems(IList selectedItems) - { - if (selectedItems is INotifyCollectionChanged incc) - { - incc.CollectionChanged -= SelectedItemsCollectionChanged; - } - } - - private void UnsubscribeFromSelectionModel(ISelectionModel model) - { - model.PropertyChanged -= SelectionModelPropertyChanged; - model.SelectionChanged -= SelectionModelSelectionChanged; - model.SourceReset -= SelectionModelSourceReset; - } - - private static int IndexOf(object? source, object? item) - { - if (source is IList l) - { - return l.IndexOf(item); - } - else if (source is ItemsSourceView v) - { - return v.IndexOf(item); - } - - return -1; - } - } -} diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs index 55e2df8890..03dcf85b87 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs @@ -38,11 +38,13 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri) { if (listenUri.Scheme != "http") - throw new ArgumentException("listenUri"); + throw new ArgumentException("URI scheme is not HTTP.", nameof(listenUri)); var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build."; _resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames() - .Where(r => r.StartsWith(resourcePrefix) && r.EndsWith(".gz")).ToDictionary( + .Where(r => r.StartsWith(resourcePrefix, StringComparison.OrdinalIgnoreCase) + && r.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)) + .ToDictionary( r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3), r => { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 1c49b24f52..bf7d0e232a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -9,7 +9,7 @@ namespace Avalonia.Diagnostics.ViewModels { internal class MainViewModel : ViewModelBase, IDisposable { - private readonly IControl _root; + private readonly TopLevel _root; private readonly TreePageViewModel _logicalTree; private readonly TreePageViewModel _visualTree; private readonly EventsPageViewModel _events; @@ -19,8 +19,10 @@ namespace Avalonia.Diagnostics.ViewModels private string _focusedControl; private string _pointerOverElement; private bool _shouldVisualizeMarginPadding = true; + private bool _shouldVisualizeDirtyRects; + private bool _showFpsOverlay; - public MainViewModel(IControl root) + public MainViewModel(TopLevel root) { _root = root; _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); @@ -40,12 +42,42 @@ namespace Avalonia.Diagnostics.ViewModels get => _shouldVisualizeMarginPadding; set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); } + + public bool ShouldVisualizeDirtyRects + { + get => _shouldVisualizeDirtyRects; + set + { + _root.Renderer.DrawDirtyRects = value; + RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); + } + } + + public void ToggleVisualizeDirtyRects() + { + ShouldVisualizeDirtyRects = !ShouldVisualizeDirtyRects; + } public void ToggleVisualizeMarginPadding() { ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding; } + public bool ShowFpsOverlay + { + get => _showFpsOverlay; + set + { + _root.Renderer.DrawFps = value; + RaiseAndSetIfChanged(ref _showFpsOverlay, value); + } + } + + public void ToggleFpsOverlay() + { + ShowFpsOverlay = !ShowFpsOverlay; + } + public ConsoleViewModel Console { get; } public ViewModelBase Content @@ -128,10 +160,7 @@ namespace Avalonia.Diagnostics.ViewModels { var tree = Content as TreePageViewModel; - if (tree != null) - { - tree.SelectControl(control); - } + tree?.SelectControl(control); } public void Dispose() @@ -140,6 +169,8 @@ namespace Avalonia.Diagnostics.ViewModels _pointerOverSubscription.Dispose(); _logicalTree.Dispose(); _visualTree.Dispose(); + _root.Renderer.DrawDirtyRects = false; + _root.Renderer.DrawFps = false; } private void UpdateFocusedControl() diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index ddbdae7ed9..e23d6f1471 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -8,8 +8,8 @@ namespace Avalonia.Diagnostics.ViewModels internal abstract class PropertyViewModel : ViewModelBase { private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; - private static readonly Type[] StringParameter = new[] { typeof(string) }; - private static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) }; + private static readonly Type[] StringParameter = { typeof(string) }; + private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; public abstract object Key { get; } public abstract string Name { get; } @@ -26,35 +26,46 @@ namespace Avalonia.Diagnostics.ViewModels } var converter = TypeDescriptor.GetConverter(value); - return converter?.ConvertToString(value) ?? value.ToString(); + + //CollectionConverter does not deliver any important information. It just displays "(Collection)". + if (!converter.CanConvertTo(typeof(string)) || + converter.GetType() == typeof(CollectionConverter)) + { + return value.ToString(); + } + + return converter.ConvertToString(value); } - protected static object ConvertFromString(string s, Type targetType) + private static object InvokeParse(string s, Type targetType) { - var converter = TypeDescriptor.GetConverter(targetType); - - if (converter != null && converter.CanConvertFrom(typeof(string))) + var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); + + if (method != null) { - return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); + return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture }); } - else + + method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null); + + if (method != null) { - var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); + return method.Invoke(null, new object[] { s }); + } - if (method != null) - { - return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture }); - } + throw new InvalidCastException("Unable to convert value."); + } - method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null); + protected static object ConvertFromString(string s, Type targetType) + { + var converter = TypeDescriptor.GetConverter(targetType); - if (method != null) - { - return method.Invoke(null, new object[] { s }); - } + if (converter.CanConvertFrom(typeof(string))) + { + return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); } - throw new InvalidCastException("Unable to convert value."); + return InvokeParse(s, targetType); } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 0165398718..8c4db33f91 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -24,6 +24,20 @@ IsEnabled="False"/> + + + + + + + + + + diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 3ae6c8c30e..4f6af0a41b 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -385,7 +385,7 @@ namespace Avalonia.Headless } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { } diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 763d192693..4c0e2982f4 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -155,9 +155,9 @@ namespace Avalonia.Headless return new List { "Arial" }; } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out Typeface typeface) { - fontKey = new FontKey("Arial", fontStyle, fontWeight); + typeface = new Typeface("Arial", fontStyle, fontWeight); return true; } } diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 96f0bb59b3..660584e2ed 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -28,7 +28,7 @@ namespace Avalonia.Input /// /// The window to which the handler belongs. /// - private IInputRoot _owner; + private IInputRoot? _owner; /// /// Whether access keys are currently being shown; @@ -48,17 +48,17 @@ namespace Avalonia.Input /// /// Element to restore following AltKey taking focus. /// - private IInputElement _restoreFocusElement; + private IInputElement? _restoreFocusElement; /// /// The window's main menu. /// - private IMainMenu _mainMenu; + private IMainMenu? _mainMenu; /// /// Gets or sets the window's main menu. /// - public IMainMenu MainMenu + public IMainMenu? MainMenu { get => _mainMenu; set @@ -86,14 +86,12 @@ namespace Avalonia.Input /// public void SetOwner(IInputRoot owner) { - Contract.Requires(owner != null); - if (_owner != null) { throw new InvalidOperationException("AccessKeyHandler owner has already been set."); } - _owner = owner; + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); _owner.AddHandler(InputElement.KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel); _owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown, RoutingStrategies.Bubble); @@ -149,7 +147,7 @@ namespace Avalonia.Input // When Alt is pressed without a main menu, or with a closed main menu, show // access key markers in the window (i.e. "_File"). - _owner.ShowAccessKeys = _showingAccessKeys = true; + _owner!.ShowAccessKeys = _showingAccessKeys = true; } else { @@ -241,7 +239,7 @@ namespace Avalonia.Input { if (_showingAccessKeys) { - _owner.ShowAccessKeys = false; + _owner!.ShowAccessKeys = false; } } @@ -250,13 +248,13 @@ namespace Avalonia.Input /// private void CloseMenu() { - MainMenu.Close(); - _owner.ShowAccessKeys = _showingAccessKeys = false; + MainMenu!.Close(); + _owner!.ShowAccessKeys = _showingAccessKeys = false; } private void MainMenuClosed(object sender, EventArgs e) { - _owner.ShowAccessKeys = false; + _owner!.ShowAccessKeys = false; } } } diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj index 2204778afe..c39c81a965 100644 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ b/src/Avalonia.Input/Avalonia.Input.csproj @@ -1,6 +1,8 @@  netstandard2.0 + Enable + CS8600;CS8602;CS8603 diff --git a/src/Avalonia.Input/DataObject.cs b/src/Avalonia.Input/DataObject.cs index 60d7d67606..688f5f9cc8 100644 --- a/src/Avalonia.Input/DataObject.cs +++ b/src/Avalonia.Input/DataObject.cs @@ -11,7 +11,7 @@ namespace Avalonia.Input return _items.ContainsKey(dataFormat); } - public object Get(string dataFormat) + public object? Get(string dataFormat) { if (_items.ContainsKey(dataFormat)) return _items[dataFormat]; @@ -23,12 +23,12 @@ namespace Avalonia.Input return _items.Keys; } - public IEnumerable GetFileNames() + public IEnumerable? GetFileNames() { return Get(DataFormats.FileNames) as IEnumerable; } - public string GetText() + public string? GetText() { return Get(DataFormats.Text) as string; } diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs index bcd962bc31..30a08eda17 100644 --- a/src/Avalonia.Input/DragDropDevice.cs +++ b/src/Avalonia.Input/DragDropDevice.cs @@ -9,9 +9,9 @@ namespace Avalonia.Input { public static readonly DragDropDevice Instance = new DragDropDevice(); - private Interactive _lastTarget = null; + private Interactive? _lastTarget = null; - private Interactive GetTarget(IInputRoot root, Point local) + private Interactive? GetTarget(IInputRoot root, Point local) { var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType()?.FirstOrDefault(); if (target != null && DragDrop.GetAllowDrop(target)) @@ -19,7 +19,7 @@ namespace Avalonia.Input return null; } - private DragDropEffects RaiseDragEvent(Interactive target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers) + private DragDropEffects RaiseDragEvent(Interactive? target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers) { if (target == null) return DragDropEffects.None; diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 66355da8b9..a1f1478f51 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -15,8 +15,8 @@ namespace Avalonia.Input /// /// The focus scopes in which the focus is currently defined. /// - private readonly ConditionalWeakTable _focusScopes = - new ConditionalWeakTable(); + private readonly ConditionalWeakTable _focusScopes = + new ConditionalWeakTable(); /// /// Initializes a new instance of the class. @@ -37,12 +37,12 @@ namespace Avalonia.Input /// /// Gets the currently focused . /// - public IInputElement Current => KeyboardDevice.Instance?.FocusedElement; + public IInputElement? Current => KeyboardDevice.Instance?.FocusedElement; /// /// Gets the current focus scope. /// - public IFocusScope Scope + public IFocusScope? Scope { get; private set; @@ -55,7 +55,7 @@ namespace Avalonia.Input /// The method by which focus was changed. /// Any key modifiers active at the time of focus. public void Focus( - IInputElement control, + IInputElement? control, NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None) { @@ -75,17 +75,18 @@ namespace Avalonia.Input // If control is null, set focus to the topmost focus scope. foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList()) { - IInputElement element; - - if (_focusScopes.TryGetValue(scope, out element) && element != null) + if (_focusScopes.TryGetValue(scope, out var element) && element != null) { Focus(element, method); return; } } - // Couldn't find a focus scope, clear focus. - SetFocusedElement(Scope, null); + if (Scope is object) + { + // Couldn't find a focus scope, clear focus. + SetFocusedElement(Scope, null); + } } } @@ -102,13 +103,13 @@ namespace Avalonia.Input /// public void SetFocusedElement( IFocusScope scope, - IInputElement element, + IInputElement? element, NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None) { - Contract.Requires(scope != null); + scope = scope ?? throw new ArgumentNullException(nameof(scope)); - if (_focusScopes.TryGetValue(scope, out IInputElement existingElement)) + if (_focusScopes.TryGetValue(scope, out var existingElement)) { if (element != existingElement) { @@ -133,11 +134,9 @@ namespace Avalonia.Input /// The new focus scope. public void SetFocusScope(IFocusScope scope) { - Contract.Requires(scope != null); + scope = scope ?? throw new ArgumentNullException(nameof(scope)); - IInputElement e; - - if (!_focusScopes.TryGetValue(scope, out e)) + if (!_focusScopes.TryGetValue(scope, out var e)) { // TODO: Make this do something useful, i.e. select the first focusable // control, select a control that the user has specified to have default @@ -164,17 +163,19 @@ namespace Avalonia.Input /// The focus scopes. private static IEnumerable GetFocusScopeAncestors(IInputElement control) { - while (control != null) + IInputElement? c = control; + + while (c != null) { - var scope = control as IFocusScope; + var scope = c as IFocusScope; - if (scope != null && control.VisualRoot?.IsVisible == true) + if (scope != null && c.VisualRoot?.IsVisible == true) { yield return scope; } - control = control.GetVisualParent() ?? - ((control as IHostedVisualTreeRoot)?.Host as IInputElement); + c = c.GetVisualParent() ?? + ((c as IHostedVisualTreeRoot)?.Host as IInputElement); } } @@ -190,7 +191,7 @@ namespace Avalonia.Input if (sender == e.Source && ev.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { - IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement; + IVisual? element = ev.Pointer?.Captured ?? e.Source as IInputElement; while (element != null) { diff --git a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs index 112abb1a4e..54ef0b1a68 100644 --- a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs +++ b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -1,8 +1,6 @@ -using System; using System.Collections; using System.Collections.Generic; using Avalonia.Controls; -using Avalonia.Data; using Avalonia.LogicalTree; using Avalonia.Styling; @@ -11,8 +9,8 @@ namespace Avalonia.Input.GestureRecognizers public class GestureRecognizerCollection : IReadOnlyCollection, IGestureRecognizerActionsDispatcher { private readonly IInputElement _inputElement; - private List _recognizers; - private Dictionary _pointerGrabs; + private List? _recognizers; + private Dictionary? _pointerGrabs; public GestureRecognizerCollection(IInputElement inputElement) @@ -72,7 +70,7 @@ namespace Avalonia.Input.GestureRecognizers { if (_recognizers == null) return false; - if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) + if (_pointerGrabs!.TryGetValue(e.Pointer, out var capture)) { capture.PointerReleased(e); } @@ -90,7 +88,7 @@ namespace Avalonia.Input.GestureRecognizers { if (_recognizers == null) return false; - if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) + if (_pointerGrabs!.TryGetValue(e.Pointer, out var capture)) { capture.PointerMoved(e); } @@ -108,7 +106,7 @@ namespace Avalonia.Input.GestureRecognizers { if (_recognizers == null) return; - _pointerGrabs.Remove(e.Pointer); + _pointerGrabs!.Remove(e.Pointer); foreach (var r in _recognizers) { r.PointerCaptureLost(e.Pointer); @@ -118,8 +116,8 @@ namespace Avalonia.Input.GestureRecognizers void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer) { pointer.Capture(_inputElement); - _pointerGrabs[pointer] = recognizer; - foreach (var r in _recognizers) + _pointerGrabs![pointer] = recognizer; + foreach (var r in _recognizers!) { if (r != recognizer) r.PointerCaptureLost(pointer); diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs index e022401c8e..3858cc04f2 100644 --- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using Avalonia.Interactivity; using Avalonia.Threading; namespace Avalonia.Input.GestureRecognizers @@ -11,9 +10,9 @@ namespace Avalonia.Input.GestureRecognizers { private bool _scrolling; private Point _trackedRootPoint; - private IPointer _tracking; - private IInputElement _target; - private IGestureRecognizerActionsDispatcher _actions; + private IPointer? _tracking; + private IInputElement? _target; + private IGestureRecognizerActionsDispatcher? _actions; private bool _canHorizontallyScroll; private bool _canVerticallyScroll; private int _gestureId; @@ -95,7 +94,7 @@ namespace Avalonia.Input.GestureRecognizers _scrolling = true; if (_scrolling) { - _actions.Capture(e.Pointer, this); + _actions!.Capture(e.Pointer, this); } } @@ -110,7 +109,7 @@ namespace Avalonia.Input.GestureRecognizers _trackedRootPoint = rootPoint; if (elapsed.TotalSeconds > 0) _inertia = vector / elapsed.TotalSeconds; - _target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); + _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); e.Handled = true; } } @@ -128,7 +127,7 @@ namespace Avalonia.Input.GestureRecognizers { _inertia = default; _scrolling = false; - _target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); + _target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); _gestureId = 0; _lastMoveTimestamp = null; } @@ -165,7 +164,7 @@ namespace Avalonia.Input.GestureRecognizers var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); var distance = speed * elapsedSinceLastTick.TotalSeconds; - _target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); + _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index cdfcd94692..1be2595ebe 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -29,7 +29,9 @@ namespace Avalonia.Input RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. private static WeakReference s_lastPress = new WeakReference(null); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. static Gestures() { @@ -69,6 +71,11 @@ namespace Avalonia.Input private static void PointerPressed(RoutedEventArgs ev) { + if (ev.Source is null) + { + return; + } + if (ev.Route == RoutingStrategies.Bubble) { var e = (PointerPressedEventArgs)ev; @@ -76,7 +83,7 @@ namespace Avalonia.Input if (e.ClickCount <= 1) { - s_lastPress = new WeakReference(e.Source); + s_lastPress = new WeakReference(ev.Source); } else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { diff --git a/src/Avalonia.Input/IAccessKeyHandler.cs b/src/Avalonia.Input/IAccessKeyHandler.cs index 3e6510320f..e484d003c7 100644 --- a/src/Avalonia.Input/IAccessKeyHandler.cs +++ b/src/Avalonia.Input/IAccessKeyHandler.cs @@ -8,7 +8,7 @@ namespace Avalonia.Input /// /// Gets or sets the window's main menu. /// - IMainMenu MainMenu { get; set; } + IMainMenu? MainMenu { get; set; } /// /// Sets the owner of the access key handler. diff --git a/src/Avalonia.Input/IDataObject.cs b/src/Avalonia.Input/IDataObject.cs index 1aa8fd63d5..1db008aa3a 100644 --- a/src/Avalonia.Input/IDataObject.cs +++ b/src/Avalonia.Input/IDataObject.cs @@ -23,17 +23,17 @@ namespace Avalonia.Input /// Returns the dragged text if the DataObject contains any text. /// /// - string GetText(); + string? GetText(); /// /// Returns a list of filenames if the DataObject contains filenames. /// /// - IEnumerable GetFileNames(); + IEnumerable? GetFileNames(); /// /// Tries to get the data of the given DataFormat. /// - object Get(string dataFormat); + object? Get(string dataFormat); } } diff --git a/src/Avalonia.Input/IFocusManager.cs b/src/Avalonia.Input/IFocusManager.cs index 9122cc428d..e1b5087c3d 100644 --- a/src/Avalonia.Input/IFocusManager.cs +++ b/src/Avalonia.Input/IFocusManager.cs @@ -8,12 +8,12 @@ namespace Avalonia.Input /// /// Gets the currently focused . /// - IInputElement Current { get; } + IInputElement? Current { get; } /// /// Gets the current focus scope. /// - IFocusScope Scope { get; } + IFocusScope? Scope { get; } /// /// Focuses a control. @@ -22,7 +22,7 @@ namespace Avalonia.Input /// The method by which focus was changed. /// Any key modifiers active at the time of focus. void Focus( - IInputElement control, + IInputElement? control, NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None); diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index c30d74c965..12fec82368 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -78,7 +78,7 @@ namespace Avalonia.Input /// /// Gets or sets the associated mouse cursor. /// - Cursor Cursor { get; } + Cursor? Cursor { get; } /// /// Gets a value indicating whether this control and all its parents are enabled. diff --git a/src/Avalonia.Input/IInputRoot.cs b/src/Avalonia.Input/IInputRoot.cs index eeb7e4323e..3e2b8cc477 100644 --- a/src/Avalonia.Input/IInputRoot.cs +++ b/src/Avalonia.Input/IInputRoot.cs @@ -20,7 +20,7 @@ namespace Avalonia.Input /// /// Gets or sets the input element that the pointer is currently over. /// - IInputElement PointerOverElement { get; set; } + IInputElement? PointerOverElement { get; set; } /// /// Gets or sets a value indicating whether access keys are shown in the window. @@ -31,6 +31,6 @@ namespace Avalonia.Input /// Gets associated mouse device /// [CanBeNull] - IMouseDevice MouseDevice { get; } + IMouseDevice? MouseDevice { get; } } } diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index ba7e0484ee..9506dc36fb 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -58,10 +58,10 @@ namespace Avalonia.Input public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged { - IInputElement FocusedElement { get; } + IInputElement? FocusedElement { get; } void SetFocusedElement( - IInputElement element, + IInputElement? element, NavigationMethod method, KeyModifiers modifiers); } diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs index a3f051ce7f..7af48cef82 100644 --- a/src/Avalonia.Input/IPointer.cs +++ b/src/Avalonia.Input/IPointer.cs @@ -3,8 +3,8 @@ namespace Avalonia.Input public interface IPointer { int Id { get; } - void Capture(IInputElement control); - IInputElement Captured { get; } + void Capture(IInputElement? control); + IInputElement? Captured { get; } PointerType Type { get; } bool IsPrimary { get; } diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs index bf001dda15..1f82cb1ed7 100644 --- a/src/Avalonia.Input/IPointerDevice.cs +++ b/src/Avalonia.Input/IPointerDevice.cs @@ -6,10 +6,10 @@ namespace Avalonia.Input public interface IPointerDevice : IInputDevice { [Obsolete("Use IPointer")] - IInputElement Captured { get; } + IInputElement? Captured { get; } [Obsolete("Use IPointer")] - void Capture(IInputElement control); + void Capture(IInputElement? control); [Obsolete("Use PointerEventArgs.GetPosition")] Point GetPosition(IVisual relativeTo); diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 0616c70d82..9ace7fd92d 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Input.GestureRecognizers; using Avalonia.Interactivity; @@ -12,6 +13,7 @@ namespace Avalonia.Input /// /// Implements input-related functionality for a control. /// + [PseudoClasses(":disabled", ":focus", ":focus-visible", ":pointerover")] public class InputElement : Interactive, IInputElement { /// @@ -37,8 +39,8 @@ namespace Avalonia.Input /// /// Gets or sets associated mouse cursor. /// - public static readonly StyledProperty CursorProperty = - AvaloniaProperty.Register(nameof(Cursor), null, true); + public static readonly StyledProperty CursorProperty = + AvaloniaProperty.Register(nameof(Cursor), null, true); /// /// Defines the property. @@ -160,7 +162,7 @@ namespace Avalonia.Input private bool _isFocused; private bool _isFocusVisible; private bool _isPointerOver; - private GestureRecognizerCollection _gestureRecognizers; + private GestureRecognizerCollection? _gestureRecognizers; /// /// Initializes static members of the class. @@ -336,7 +338,7 @@ namespace Avalonia.Input /// /// Gets or sets associated mouse cursor. /// - public Cursor Cursor + public Cursor? Cursor { get { return GetValue(CursorProperty); } set { SetValue(CursorProperty, value); } diff --git a/src/Avalonia.Input/KeyEventArgs.cs b/src/Avalonia.Input/KeyEventArgs.cs index 267376262b..67cd5a520a 100644 --- a/src/Avalonia.Input/KeyEventArgs.cs +++ b/src/Avalonia.Input/KeyEventArgs.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input { public class KeyEventArgs : RoutedEventArgs { - public IKeyboardDevice Device { get; set; } + public IKeyboardDevice? Device { get; set; } public Key Key { get; set; } diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index ad447794bc..aa6fcc8bff 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; using System.Text; namespace Avalonia.Input @@ -29,7 +27,7 @@ namespace Avalonia.Input KeyModifiers = modifiers; } - public bool Equals(KeyGesture other) + public bool Equals(KeyGesture? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; @@ -37,12 +35,12 @@ namespace Avalonia.Input return Key == other.Key && KeyModifiers == other.KeyModifiers; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - return obj is KeyGesture && Equals((KeyGesture)obj); + return obj is KeyGesture gesture && Equals(gesture); } public override int GetHashCode() @@ -53,12 +51,12 @@ namespace Avalonia.Input } } - public static bool operator ==(KeyGesture left, KeyGesture right) + public static bool operator ==(KeyGesture? left, KeyGesture? right) { return Equals(left, right); } - public static bool operator !=(KeyGesture left, KeyGesture right) + public static bool operator !=(KeyGesture? left, KeyGesture? right) { return !Equals(left, right); } diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 0321b0bdf3..187670a26b 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -8,9 +8,9 @@ namespace Avalonia.Input { public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged { - private IInputElement _focusedElement; + private IInputElement? _focusedElement; - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; public static IKeyboardDevice Instance => AvaloniaLocator.Current.GetService(); @@ -18,7 +18,7 @@ namespace Avalonia.Input public IFocusManager FocusManager => AvaloniaLocator.Current.GetService(); - public IInputElement FocusedElement + public IInputElement? FocusedElement { get { @@ -33,7 +33,7 @@ namespace Avalonia.Input } public void SetFocusedElement( - IInputElement element, + IInputElement? element, NavigationMethod method, KeyModifiers keyModifiers) { diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs index 722215f8b7..6ef3c4fd60 100644 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ b/src/Avalonia.Input/KeyboardNavigation.cs @@ -25,12 +25,11 @@ namespace Avalonia.Input /// attached property set to , this property /// defines to which child the focus should move. /// - public static readonly AttachedProperty TabOnceActiveElementProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty TabOnceActiveElementProperty = + AvaloniaProperty.RegisterAttached( "TabOnceActiveElement", typeof(KeyboardNavigation)); - /// /// Defines the IsTabStop attached property. /// @@ -68,7 +67,7 @@ namespace Avalonia.Input /// /// The container. /// The active element for the container. - public static IInputElement GetTabOnceActiveElement(InputElement element) + public static IInputElement? GetTabOnceActiveElement(InputElement element) { return element.GetValue(TabOnceActiveElementProperty); } @@ -78,7 +77,7 @@ namespace Avalonia.Input /// /// The container. /// The active element for the container. - public static void SetTabOnceActiveElement(InputElement element, IInputElement value) + public static void SetTabOnceActiveElement(InputElement element, IInputElement? value) { element.SetValue(TabOnceActiveElementProperty, value); } diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index c425eeeedb..dbefe63789 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -13,7 +13,7 @@ namespace Avalonia.Input /// /// The window to which the handler belongs. /// - private IInputRoot _owner; + private IInputRoot? _owner; /// /// Sets the owner of the keyboard navigation handler. @@ -24,15 +24,12 @@ namespace Avalonia.Input /// public void SetOwner(IInputRoot owner) { - Contract.Requires(owner != null); - if (_owner != null) { throw new InvalidOperationException("AccessKeyHandler owner has already been set."); } - _owner = owner; - + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); _owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown); } @@ -45,11 +42,11 @@ namespace Avalonia.Input /// The next element in the specified direction, or null if /// was the last in the requested direction. /// - public static IInputElement GetNext( + public static IInputElement? GetNext( IInputElement element, NavigationDirection direction) { - Contract.Requires(element != null); + element = element ?? throw new ArgumentNullException(nameof(element)); var customHandler = element.GetSelfAndVisualAncestors() .OfType() @@ -97,7 +94,7 @@ namespace Avalonia.Input NavigationDirection direction, KeyModifiers keyModifiers = KeyModifiers.None) { - Contract.Requires(element != null); + element = element ?? throw new ArgumentNullException(nameof(element)); var next = GetNext(element, direction); diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 188ddd9835..cec5029c18 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -20,7 +20,7 @@ namespace Avalonia.Input private readonly Pointer _pointer; private bool _disposed; - public MouseDevice(Pointer pointer = null) + public MouseDevice(Pointer? pointer = null) { _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); } @@ -34,7 +34,7 @@ namespace Avalonia.Input /// method. /// [Obsolete("Use IPointer instead")] - public IInputElement Captured => _pointer.Captured; + public IInputElement? Captured => _pointer.Captured; /// /// Gets the mouse position, in screen coordinates. @@ -54,7 +54,7 @@ namespace Avalonia.Input /// within the control's bounds or not. The current mouse capture control is exposed /// by the property. /// - public void Capture(IInputElement control) + public void Capture(IInputElement? control) { _pointer.Capture(control); } @@ -66,7 +66,7 @@ namespace Avalonia.Input /// The mouse position in the control's coordinates. public Point GetPosition(IVisual relativeTo) { - Contract.Requires(relativeTo != null); + relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); if (relativeTo.VisualRoot == null) { @@ -75,7 +75,7 @@ namespace Avalonia.Input var rootPoint = relativeTo.VisualRoot.PointToClient(Position); var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); - return rootPoint * transform.Value; + return rootPoint * transform!.Value; } public void ProcessRawEvent(RawInputEventArgs e) @@ -126,7 +126,7 @@ namespace Avalonia.Input private void ProcessRawEvent(RawPointerEventArgs e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); var mouse = (MouseDevice)e.Device; if(mouse._disposed) @@ -173,8 +173,8 @@ namespace Avalonia.Input private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); ClearPointerOver(this, timestamp, root, properties, inputModifiers); } @@ -214,8 +214,8 @@ namespace Avalonia.Input PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var hit = HitTest(root, p); @@ -250,10 +250,10 @@ namespace Avalonia.Input private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); - IInputElement source; + IInputElement? source; if (_pointer.Captured == null) { @@ -265,18 +265,23 @@ namespace Avalonia.Input source = _pointer.Captured; } - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, - p, timestamp, properties, inputModifiers); + if (source is object) + { + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, + p, timestamp, properties, inputModifiers); - source?.RaiseEvent(e); - return e.Handled; + source.RaiseEvent(e); + return e.Handled; + } + + return false; } private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var hit = HitTest(root, p); @@ -298,8 +303,8 @@ namespace Avalonia.Input PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var hit = HitTest(root, p); @@ -317,21 +322,21 @@ namespace Avalonia.Input private IInteractive GetSource(IVisual hit) { - Contract.Requires(hit != null); + hit = hit ?? throw new ArgumentNullException(nameof(hit)); return _pointer.Captured ?? (hit as IInteractive) ?? hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); } - private IInputElement HitTest(IInputElement root, Point p) + private IInputElement? HitTest(IInputElement root, Point p) { - Contract.Requires(root != null); + root = root ?? throw new ArgumentNullException(nameof(root)); return _pointer.Captured ?? root.InputHitTest(p); } - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, + PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, PointerPointProperties properties, KeyModifiers inputModifiers) { @@ -343,8 +348,8 @@ namespace Avalonia.Input PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var element = root.PointerOverElement; var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); @@ -384,12 +389,12 @@ namespace Avalonia.Input } } - private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, + private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); var element = root.InputHitTest(p); @@ -412,11 +417,11 @@ namespace Avalonia.Input PointerPointProperties properties, KeyModifiers inputModifiers) { - Contract.Requires(device != null); - Contract.Requires(root != null); - Contract.Requires(element != null); + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + element = element ?? throw new ArgumentNullException(nameof(element)); - IInputElement branch = null; + IInputElement? branch = null; var el = element; diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index cd377f1df6..6f6d68940b 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -22,15 +22,17 @@ namespace Avalonia.Input.Navigation /// The next element in the specified direction, or null if /// was the last in the requested direction. /// - public static IInputElement GetNextInTabOrder( + public static IInputElement? GetNextInTabOrder( IInputElement element, NavigationDirection direction, bool outsideElement = false) { - Contract.Requires(element != null); - Contract.Requires( - direction == NavigationDirection.Next || - direction == NavigationDirection.Previous); + element = element ?? throw new ArgumentNullException(nameof(element)); + + if (direction != NavigationDirection.Next && direction != NavigationDirection.Previous) + { + throw new ArgumentException("Invalid direction: must be Next or Previous."); + } var container = element.GetVisualParent(); @@ -110,7 +112,7 @@ namespace Avalonia.Input.Navigation if (customNext.handled) { - yield return customNext.next; + yield return customNext.next!; } else { @@ -143,12 +145,14 @@ namespace Avalonia.Input.Navigation /// If true will not descend into to find next control. /// /// The next element, or null if the element is the last. - private static IInputElement GetNextInContainer( + private static IInputElement? GetNextInContainer( IInputElement element, IInputElement container, NavigationDirection direction, bool outsideElement) { + IInputElement? e = element; + if (direction == NavigationDirection.Next && !outsideElement) { var descendant = GetFocusableDescendants(element, direction).FirstOrDefault(); @@ -167,13 +171,13 @@ namespace Avalonia.Input.Navigation // INavigableContainer. if (navigable != null) { - while (element != null) + while (e != null) { - element = navigable.GetControl(direction, element, false); + e = navigable.GetControl(direction, e, false); - if (element != null && - element.CanFocus() && - KeyboardNavigation.GetIsTabStop((InputElement) element)) + if (e != null && + e.CanFocus() && + KeyboardNavigation.GetIsTabStop((InputElement)e)) { break; } @@ -183,12 +187,12 @@ namespace Avalonia.Input.Navigation { // TODO: Do a spatial search here if the container doesn't implement // INavigableContainer. - element = null; + e = null; } - if (element != null && direction == NavigationDirection.Previous) + if (e != null && direction == NavigationDirection.Previous) { - var descendant = GetFocusableDescendants(element, direction).LastOrDefault(); + var descendant = GetFocusableDescendants(e, direction).LastOrDefault(); if (descendant != null) { @@ -196,7 +200,7 @@ namespace Avalonia.Input.Navigation } } - return element; + return e; } return null; @@ -209,13 +213,13 @@ namespace Avalonia.Input.Navigation /// The container. /// The direction of the search. /// The first element, or null if there are no more elements. - private static IInputElement GetFirstInNextContainer( + private static IInputElement? GetFirstInNextContainer( IInputElement element, IInputElement container, NavigationDirection direction) { var parent = container.GetVisualParent(); - IInputElement next = null; + IInputElement? next = null; if (parent != null) { @@ -268,7 +272,7 @@ namespace Avalonia.Input.Navigation return next; } - private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, + private static (bool handled, IInputElement? next) GetCustomNext(IInputElement element, NavigationDirection direction) { if (element is ICustomKeyboardNavigation custom) diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index 00222e92cf..a477711584 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/src/Avalonia.Input/Pointer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Interactivity; using Avalonia.VisualTree; namespace Avalonia.Input @@ -20,7 +19,7 @@ namespace Avalonia.Input public int Id { get; } - IInputElement FindCommonParent(IInputElement control1, IInputElement control2) + IInputElement? FindCommonParent(IInputElement? control1, IInputElement? control2) { if (control1 == null || control2 == null) return null; @@ -28,12 +27,12 @@ namespace Avalonia.Input return control2.GetSelfAndVisualAncestors().OfType().FirstOrDefault(seen.Contains); } - protected virtual void PlatformCapture(IInputElement element) + protected virtual void PlatformCapture(IInputElement? element) { } - public void Capture(IInputElement control) + public void Capture(IInputElement? control) { if (Captured != null) Captured.DetachedFromVisualTree -= OnCaptureDetached; @@ -66,7 +65,7 @@ namespace Avalonia.Input } - public IInputElement Captured { get; private set; } + public IInputElement? Captured { get; private set; } public PointerType Type { get; } public bool IsPrimary { get; } diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 9cc42ffa69..451f80b1df 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -7,14 +7,14 @@ namespace Avalonia.Input { public class PointerEventArgs : RoutedEventArgs { - private readonly IVisual _rootVisual; + private readonly IVisual? _rootVisual; private readonly Point _rootVisualPosition; private readonly PointerPointProperties _properties; public PointerEventArgs(RoutedEvent routedEvent, - IInteractive source, + IInteractive? source, IPointer pointer, - IVisual rootVisual, Point rootVisualPosition, + IVisual? rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers) @@ -40,8 +40,8 @@ namespace Avalonia.Input public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException(); - public IInputElement Captured => _ev.Pointer.Captured; - public void Capture(IInputElement control) + public IInputElement? Captured => _ev.Pointer.Captured; + public void Capture(IInputElement? control) { _ev.Pointer.Capture(control); } @@ -52,7 +52,7 @@ namespace Avalonia.Input public IPointer Pointer { get; } public ulong Timestamp { get; } - private IPointerDevice _device; + private IPointerDevice? _device; [Obsolete("Use Pointer to get pointer-specific information")] public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this)); @@ -76,7 +76,7 @@ namespace Avalonia.Input public KeyModifiers KeyModifiers { get; } - public Point GetPosition(IVisual relativeTo) + public Point GetPosition(IVisual? relativeTo) { if (_rootVisual == null) return default; @@ -86,14 +86,14 @@ namespace Avalonia.Input } [Obsolete("Use GetCurrentPoint")] - public PointerPoint GetPointerPoint(IVisual relativeTo) => GetCurrentPoint(relativeTo); + public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo); /// /// Returns the PointerPoint associated with the current event /// /// The visual which coordinate system to use. Pass null for toplevel coordinate system /// - public PointerPoint GetCurrentPoint(IVisual relativeTo) + public PointerPoint GetCurrentPoint(IVisual? relativeTo) => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); /// diff --git a/src/Avalonia.Input/Raw/RawDragEventType.cs b/src/Avalonia.Input/Raw/RawDragEventType.cs index 9635f77467..77f17a5a41 100644 --- a/src/Avalonia.Input/Raw/RawDragEventType.cs +++ b/src/Avalonia.Input/Raw/RawDragEventType.cs @@ -7,4 +7,4 @@ DragLeave, Drop } -} \ No newline at end of file +} diff --git a/src/Avalonia.Input/Raw/RawInputEventArgs.cs b/src/Avalonia.Input/Raw/RawInputEventArgs.cs index b85563b24a..dcc5f27a79 100644 --- a/src/Avalonia.Input/Raw/RawInputEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawInputEventArgs.cs @@ -21,7 +21,7 @@ namespace Avalonia.Input.Raw /// The root from which the event originates. public RawInputEventArgs(IInputDevice device, ulong timestamp, IInputRoot root) { - Contract.Requires(device != null); + device = device ?? throw new ArgumentNullException(nameof(device)); Device = device; Timestamp = timestamp; diff --git a/src/Avalonia.Input/TextInputEventArgs.cs b/src/Avalonia.Input/TextInputEventArgs.cs index 6e763d3b56..cda0103749 100644 --- a/src/Avalonia.Input/TextInputEventArgs.cs +++ b/src/Avalonia.Input/TextInputEventArgs.cs @@ -4,8 +4,8 @@ namespace Avalonia.Input { public class TextInputEventArgs : RoutedEventArgs { - public IKeyboardDevice Device { get; set; } + public IKeyboardDevice? Device { get; set; } - public string Text { get; set; } + public string? Text { get; set; } } } diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index cd7f725f18..eace54d2e0 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -211,7 +211,7 @@ namespace Avalonia.Layout anchorPosition = new Point(anchorBounds.X, anchorBounds.Y); } } - else + else if (anchorIndex >= 0) { // It is possible to end up in a situation during a collection change where GetAnchorForTargetElement returns an index // which is not in the realized range. Eg. insert one item at index 0 for a grid layout. diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index e62e22f8ec..aca2965ea6 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -758,8 +758,6 @@ namespace Avalonia.Layout protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { - base.OnDetachedFromVisualTreeCore(e); - if (e.Root is ILayoutRoot r) { if (_layoutUpdated is object) @@ -772,6 +770,8 @@ namespace Avalonia.Layout r.LayoutManager.UnregisterEffectiveViewportListener(this); } } + + base.OnDetachedFromVisualTreeCore(e); } /// diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index f084411c2f..49bd578290 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -1,7 +1,8 @@  - false + $([MSBuild]::IsOSPlatform(OSX)) + $(PackAvaloniaNative) true netstandard2.0 /usr/bin/castxml @@ -10,8 +11,9 @@ false - + + libAvaloniaNative.dylib runtimes/osx/native/libAvaloniaNative.dylib true PreserveNewest @@ -26,4 +28,4 @@ - + \ No newline at end of file diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 804cf7f8ac..e8b2f065c7 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -16,7 +16,7 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private AvaloniaNativePlatformOptions _options; - private GlPlatformFeature _glFeature; + private AvaloniaNativePlatformOpenGlInterface _platformGl; [DllImport("libAvaloniaNative")] static extern IntPtr CreateAvaloniaNative(); @@ -116,8 +116,8 @@ namespace Avalonia.Native { try { - AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay())); + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(_platformGl = new AvaloniaNativePlatformOpenGlInterface(_factory.ObtainGlDisplay())); } catch (Exception) { @@ -128,7 +128,7 @@ namespace Avalonia.Native public IWindowImpl CreateWindow() { - return new WindowImpl(_factory, _options, _glFeature); + return new WindowImpl(_factory, _options, _platformGl); } public IWindowImpl CreateEmbeddableWindow() diff --git a/src/Avalonia.Native/GlPlatformFeature.cs b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs similarity index 76% rename from src/Avalonia.Native/GlPlatformFeature.cs rename to src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs index e321db6eda..dbe968b82f 100644 --- a/src/Avalonia.Native/GlPlatformFeature.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs @@ -2,21 +2,20 @@ using Avalonia.OpenGL; using Avalonia.Native.Interop; using System.Drawing; +using Avalonia.OpenGL.Surfaces; using Avalonia.Threading; namespace Avalonia.Native { - class GlPlatformFeature : IWindowingPlatformGlFeature + class AvaloniaNativePlatformOpenGlInterface : IPlatformOpenGlInterface { private readonly IAvnGlDisplay _display; - public GlPlatformFeature(IAvnGlDisplay display) + public AvaloniaNativePlatformOpenGlInterface(IAvnGlDisplay display) { _display = display; var immediate = display.CreateContext(null); - var deferred = display.CreateContext(immediate); - int major, minor; GlInterface glInterface; using (immediate.MakeCurrent()) @@ -33,19 +32,22 @@ namespace Avalonia.Native } GlDisplay = new GlDisplay(display, glInterface, immediate.SampleCount, immediate.StencilSize); - - ImmediateContext = new GlContext(GlDisplay, immediate, _version); - DeferredContext = new GlContext(GlDisplay, deferred, _version); + MainContext = new GlContext(GlDisplay, null, immediate, _version); } - internal IGlContext ImmediateContext { get; } - public IGlContext MainContext => DeferredContext; - internal GlContext DeferredContext { get; } + internal GlContext MainContext { get; } + public IGlContext PrimaryContext => MainContext; + + public bool CanShareContexts => true; + public bool CanCreateContexts => true; internal GlDisplay GlDisplay; private readonly GlVersion _version; + public IGlContext CreateSharedContext() => new GlContext(GlDisplay, + MainContext, _display.CreateContext(MainContext.Context), _version); + public IGlContext CreateContext() => new GlContext(GlDisplay, - _display.CreateContext(((GlContext)ImmediateContext).Context), _version); + null, _display.CreateContext(null), _version); } class GlDisplay @@ -72,11 +74,13 @@ namespace Avalonia.Native class GlContext : IGlContext { private readonly GlDisplay _display; + private readonly GlContext _sharedWith; public IAvnGlContext Context { get; private set; } - public GlContext(GlDisplay display, IAvnGlContext context, GlVersion version) + public GlContext(GlDisplay display, GlContext sharedWith, IAvnGlContext context, GlVersion version) { _display = display; + _sharedWith = sharedWith; Context = context; Version = version; } @@ -86,6 +90,17 @@ namespace Avalonia.Native public int SampleCount => _display.SampleCount; public int StencilSize => _display.StencilSize; public IDisposable MakeCurrent() => Context.MakeCurrent(); + public IDisposable EnsureCurrent() => MakeCurrent(); + + public bool IsSharedWith(IGlContext context) + { + var c = (GlContext)context; + return c == this + || c._sharedWith == this + || _sharedWith == context + || _sharedWith != null && _sharedWith == c._sharedWith; + } + public void Dispose() { @@ -108,7 +123,7 @@ namespace Avalonia.Native public IGlPlatformSurfaceRenderingSession BeginDraw() { - var feature = (GlPlatformFeature)AvaloniaLocator.Current.GetService(); + var feature = (AvaloniaNativePlatformOpenGlInterface)AvaloniaLocator.Current.GetService(); return new GlPlatformSurfaceRenderingSession(_context, _target.BeginDrawing()); } diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 2d246e08d2..2f98385038 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -9,12 +9,12 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; - private readonly GlPlatformFeature _glFeature; + private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; private readonly IWindowBaseImpl _parent; public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - GlPlatformFeature glFeature, + AvaloniaNativePlatformOpenGlInterface glFeature, IWindowBaseImpl parent) : base(opts, glFeature) { _factory = factory; @@ -23,7 +23,7 @@ namespace Avalonia.Native _parent = parent; using (var e = new PopupEvents(this)) { - var context = _opts.UseGpu ? glFeature?.DeferredContext : null; + var context = _opts.UseGpu ? glFeature?.MainContext : null; Init(factory.CreatePopup(e, context?.Context), factory.CreateScreens(), context); } PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 885591495b..11a0ebce61 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -14,19 +14,19 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; - private readonly GlPlatformFeature _glFeature; + private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; IAvnWindow _native; private double _extendTitleBarHeight = -1; internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - GlPlatformFeature glFeature) : base(opts, glFeature) + AvaloniaNativePlatformOpenGlInterface glFeature) : base(opts, glFeature) { _factory = factory; _opts = opts; _glFeature = glFeature; using (var e = new WindowEvents(this)) { - var context = _opts.UseGpu ? glFeature?.DeferredContext : null; + var context = _opts.UseGpu ? glFeature?.MainContext : null; Init(_native = factory.CreateWindow(e, context?.Context), factory.CreateScreens(), context); } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 56cf544d9d..5d35c773d7 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -61,7 +61,7 @@ namespace Avalonia.Native private NativeControlHostImpl _nativeControlHost; private IGlContext _glContext; - internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, GlPlatformFeature glFeature) + internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, AvaloniaNativePlatformOpenGlInterface glFeature) { _gpu = opts.UseGpu && glFeature != null; _deferredRendering = opts.UseDeferredRendering; diff --git a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs new file mode 100644 index 0000000000..8c9b028164 --- /dev/null +++ b/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs @@ -0,0 +1,37 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.OpenGL.Egl; +using Avalonia.Platform; +using Avalonia.Platform.Interop; + +namespace Avalonia.OpenGL.Angle +{ + public class AngleEglInterface : EglInterface + { + [DllImport("libegl.dll", CharSet = CharSet.Ansi)] + static extern IntPtr eglGetProcAddress(string proc); + + public AngleEglInterface() : base(LoadAngle()) + { + + } + + static Func LoadAngle() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var disp = eglGetProcAddress("eglGetPlatformDisplayEXT"); + + if (disp == IntPtr.Zero) + { + throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point"); + } + + return eglGetProcAddress; + } + + throw new PlatformNotSupportedException(); + } + + } +} diff --git a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs new file mode 100644 index 0000000000..191fb53204 --- /dev/null +++ b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Avalonia.OpenGL.Egl; +using static Avalonia.OpenGL.Egl.EglConsts; + +namespace Avalonia.OpenGL.Angle +{ + public class AngleWin32EglDisplay : EglDisplay + { + struct AngleInfo + { + public IntPtr Display { get; set; } + public AngleOptions.PlatformApi PlatformApi { get; set; } + } + + static AngleInfo CreateAngleDisplay(EglInterface _egl) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new PlatformNotSupportedException(); + var display = IntPtr.Zero; + AngleOptions.PlatformApi angleApi = default; + { + if (_egl.GetPlatformDisplayEXT == null) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); + + var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis + ?? new [] { AngleOptions.PlatformApi.DirectX11, AngleOptions.PlatformApi.DirectX9 }; + + foreach (var platformApi in allowedApis) + { + int dapi; + if (platformApi == AngleOptions.PlatformApi.DirectX9) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; + else if (platformApi == AngleOptions.PlatformApi.DirectX11) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; + else + continue; + + display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, + new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE }); + if (display != IntPtr.Zero) + { + angleApi = platformApi; + break; + } + } + + if (display == IntPtr.Zero) + throw new OpenGlException("Unable to create ANGLE display"); + return new AngleInfo { Display = display, PlatformApi = angleApi }; + } + } + + private AngleWin32EglDisplay(EglInterface egl, AngleInfo info) : base(egl, false, info.Display) + { + PlatformApi = info.PlatformApi; + } + + public AngleWin32EglDisplay(EglInterface egl) : this(egl, CreateAngleDisplay(egl)) + { + + } + + public AngleWin32EglDisplay() : this(new AngleEglInterface()) + { + + } + + public AngleOptions.PlatformApi PlatformApi { get; } + + public IntPtr GetDirect3DDevice() + { + if (!EglInterface.QueryDisplayAttribExt(Handle, EglConsts.EGL_DEVICE_EXT, out var eglDevice)) + throw new OpenGlException("Unable to get EGL_DEVICE_EXT"); + if (!EglInterface.QueryDeviceAttribExt(eglDevice, PlatformApi == AngleOptions.PlatformApi.DirectX9 ? EGL_D3D9_DEVICE_ANGLE : EGL_D3D11_DEVICE_ANGLE, out var d3dDeviceHandle)) + throw new OpenGlException("Unable to get EGL_D3D9_DEVICE_ANGLE"); + return d3dDeviceHandle; + } + + public EglSurface WrapDirect3D11Texture(EglPlatformOpenGlInterface egl, IntPtr handle) + { + if (PlatformApi != AngleOptions.PlatformApi.DirectX11) + throw new InvalidOperationException("Current platform API is " + PlatformApi); + return egl.CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); + } + } +} diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs index 4b9c04f4e6..0807eb7ab4 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -10,9 +10,12 @@ namespace Avalonia.OpenGL DirectX11 } - public List AllowedPlatformApis = new List + public IList GlProfiles { get; set; } = new List { - PlatformApi.DirectX9 + new GlVersion(GlProfileType.OpenGLES, 3, 0), + new GlVersion(GlProfileType.OpenGLES, 2, 0) }; + + public IList AllowedPlatformApis { get; set; } = null; } } diff --git a/src/Avalonia.OpenGL/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs similarity index 50% rename from src/Avalonia.OpenGL/OpenGlControlBase.cs rename to src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index 8567dcae20..33773ed8e2 100644 --- a/src/Avalonia.OpenGL/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -3,44 +3,83 @@ using Avalonia.Controls; using Avalonia.Logging; using Avalonia.Media; using Avalonia.OpenGL.Imaging; -using Avalonia.Rendering; -using Avalonia.VisualTree; using static Avalonia.OpenGL.GlConsts; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Controls { public abstract class OpenGlControlBase : Control { private IGlContext _context; - private int _fb, _texture, _renderBuffer; - private OpenGlTextureBitmap _bitmap; - private PixelSize _oldSize; + private int _fb, _depthBuffer; + private OpenGlBitmap _bitmap; + private IOpenGlBitmapAttachment _attachment; + private PixelSize _depthBufferSize; private bool _glFailed; + private bool _initialized; protected GlVersion GlVersion { get; private set; } public sealed override void Render(DrawingContext context) { if(!EnsureInitialized()) return; - + using (_context.MakeCurrent()) { - using (_bitmap.Lock()) - { - var gl = _context.GlInterface; - gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); - if (_oldSize != GetPixelSize()) - ResizeTexture(gl); - - OnOpenGlRender(gl, _fb); - gl.Flush(); - } + _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); + EnsureTextureAttachment(); + EnsureDepthBufferAttachment(_context.GlInterface); + if(!CheckFramebufferStatus(_context.GlInterface)) + return; + + OnOpenGlRender(_context.GlInterface, _fb); + _attachment.Present(); } context.DrawImage(_bitmap, new Rect(_bitmap.Size), Bounds); base.Render(context); } + + private void CheckError(GlInterface gl) + { + int err; + while ((err = gl.GetError()) != GL_NO_ERROR) + Console.WriteLine(err); + } + + void EnsureTextureAttachment() + { + _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); + if (_bitmap == null || _attachment == null || _bitmap.PixelSize != GetPixelSize()) + { + _attachment?.Dispose(); + _attachment = null; + _bitmap?.Dispose(); + _bitmap = null; + _bitmap = new OpenGlBitmap(GetPixelSize(), new Vector(96, 96)); + _attachment = _bitmap.CreateFramebufferAttachment(_context); + } + } + + void EnsureDepthBufferAttachment(GlInterface gl) + { + var size = GetPixelSize(); + if (size == _depthBufferSize && _depthBuffer != 0) + return; + + gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); + if (_depthBuffer != 0) gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); + + var oneArr = new int[1]; + gl.GenRenderbuffers(1, oneArr); + _depthBuffer = oneArr[0]; + gl.BindRenderbuffer(GL_RENDERBUFFER, _depthBuffer); + gl.RenderbufferStorage(GL_RENDERBUFFER, + GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, + size.Width, size.Height); + gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthBuffer); + gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); + } - void DoCleanup(bool callUserDeinit) + void DoCleanup() { if (_context != null) { @@ -50,16 +89,19 @@ namespace Avalonia.OpenGL gl.BindTexture(GL_TEXTURE_2D, 0); gl.BindFramebuffer(GL_FRAMEBUFFER, 0); gl.DeleteFramebuffers(1, new[] { _fb }); - using (_bitmap.Lock()) - _bitmap.SetTexture(0, 0, new PixelSize(1, 1), 1); - gl.DeleteTextures(1, new[] { _texture }); - gl.DeleteRenderbuffers(1, new[] { _renderBuffer }); - _bitmap.Dispose(); + gl.DeleteRenderbuffers(1, new[] { _depthBuffer }); + _attachment?.Dispose(); + _attachment = null; + _bitmap?.Dispose(); + _bitmap = null; try { - if (callUserDeinit) + if (_initialized) + { + _initialized = false; OnOpenGlDeinit(_context.GlInterface, _fb); + } } finally { @@ -72,11 +114,11 @@ namespace Avalonia.OpenGL protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { - DoCleanup(true); + DoCleanup(); base.OnDetachedFromVisualTree(e); } - bool EnsureInitialized() + private bool EnsureInitializedCore() { if (_context != null) return true; @@ -84,34 +126,43 @@ namespace Avalonia.OpenGL if (_glFailed) return false; - var feature = AvaloniaLocator.Current.GetService(); + var feature = AvaloniaLocator.Current.GetService(); if (feature == null) return false; + if (!feature.CanShareContexts) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: current platform does not support multithreaded context sharing"); + return false; + } try { - _context = feature.CreateContext(); - + _context = feature.CreateSharedContext(); } catch (Exception e) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", "Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e); - _glFailed = true; return false; } GlVersion = _context.Version; try { - _bitmap = new OpenGlTextureBitmap(); + _bitmap = new OpenGlBitmap(GetPixelSize(), new Vector(96, 96)); + if (!_bitmap.SupportsContext(_context)) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create OpenGlBitmap: OpenGL context is not compatible"); + return false; + } } catch (Exception e) { _context.Dispose(); _context = null; Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create OpenGlTextureBitmap: {exception}", e); - _glFailed = true; + "Unable to initialize OpenGL: unable to create OpenGlBitmap: {exception}", e); return false; } @@ -119,80 +170,55 @@ namespace Avalonia.OpenGL { try { - _oldSize = GetPixelSize(); + _depthBufferSize = GetPixelSize(); var gl = _context.GlInterface; var oneArr = new int[1]; gl.GenFramebuffers(1, oneArr); _fb = oneArr[0]; gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); - - gl.GenTextures(1, oneArr); - _texture = oneArr[0]; - ResizeTexture(gl); - - gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); + EnsureDepthBufferAttachment(gl); + EnsureTextureAttachment(); - var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); - if (status != GL_FRAMEBUFFER_COMPLETE) - { - int code; - while ((code = gl.GetError()) != 0) - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL FBO: {code}", code); - - _glFailed = true; - return false; - } + return CheckFramebufferStatus(gl); } catch(Exception e) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", "Unable to initialize OpenGL FBO: {exception}", e); - _glFailed = true; + return false; } - - if (!_glFailed) - OnOpenGlInit(_context.GlInterface, _fb); } + } - if (_glFailed) + private bool CheckFramebufferStatus(GlInterface gl) + { + var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { - DoCleanup(false); + int code; + while ((code = gl.GetError()) != 0) + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL FBO: {code}", code); + return false; } return true; } - void ResizeTexture(GlInterface gl) + private bool EnsureInitialized() { - var size = GetPixelSize(); - - gl.GetIntegerv( GL_TEXTURE_BINDING_2D, out var oldTexture); - gl.BindTexture(GL_TEXTURE_2D, _texture); - gl.TexImage2D(GL_TEXTURE_2D, 0, - GlVersion.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8, - size.Width, size.Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); - gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - gl.BindTexture(GL_TEXTURE_2D, oldTexture); - - gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); - gl.DeleteRenderbuffers(1, new[] { _renderBuffer }); - var oneArr = new int[1]; - gl.GenRenderbuffers(1, oneArr); - _renderBuffer = oneArr[0]; - gl.BindRenderbuffer(GL_RENDERBUFFER, _renderBuffer); - gl.RenderbufferStorage(GL_RENDERBUFFER, - GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, - size.Width, size.Height); - gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _renderBuffer); - gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); - using (_bitmap.Lock()) - _bitmap.SetTexture(_texture, GL_RGBA8, size, 1); + if (_initialized) + return true; + _glFailed = !(_initialized = EnsureInitializedCore()); + if (_glFailed) + return false; + using (_context.MakeCurrent()) + OnOpenGlInit(_context.GlInterface, _fb); + return true; } - PixelSize GetPixelSize() + private PixelSize GetPixelSize() { var scaling = VisualRoot.RenderScaling; return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)), diff --git a/src/Avalonia.OpenGL/EglConsts.cs b/src/Avalonia.OpenGL/Egl/EglConsts.cs similarity index 95% rename from src/Avalonia.OpenGL/EglConsts.cs rename to src/Avalonia.OpenGL/Egl/EglConsts.cs index 62fb3faef6..58f5f1cef5 100644 --- a/src/Avalonia.OpenGL/EglConsts.cs +++ b/src/Avalonia.OpenGL/Egl/EglConsts.cs @@ -1,6 +1,6 @@ // ReSharper disable UnusedMember.Global // ReSharper disable IdentifierTypo -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public static class EglConsts { @@ -186,11 +186,24 @@ namespace Avalonia.OpenGL public const int EGL_PLATFORM_ANGLE_TYPE_DEFAULT_ANGLE = 0x3206; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_HARDWARE_ANGLE = 0x320A; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_NULL_ANGLE = 0x345E; + + public const int EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE = 0x320D; + public const int EGL_PLATFORM_ANGLE_TYPE_OPENGLES_ANGLE = 0x320E; //EGL_ANGLE_platform_angle_d3d public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE = 0x3209; public const int EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE = 0x320F; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_WARP_ANGLE = 0x320B; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_REFERENCE_ANGLE = 0x320C; + + //EXT_device_query + public const int EGL_DEVICE_EXT = 0x322C; + + //ANGLE_device_d3d + public const int EGL_D3D9_DEVICE_ANGLE = 0x33A0; + public const int EGL_D3D11_DEVICE_ANGLE = 0x33A1; + + public const int EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE = 0x3200; + public const int EGL_D3D_TEXTURE_ANGLE = 0x33A3; } } diff --git a/src/Avalonia.OpenGL/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs similarity index 55% rename from src/Avalonia.OpenGL/EglContext.cs rename to src/Avalonia.OpenGL/Egl/EglContext.cs index 871665e857..5365354418 100644 --- a/src/Avalonia.OpenGL/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -1,23 +1,25 @@ using System; using System.Reactive.Disposables; using System.Threading; -using static Avalonia.OpenGL.EglConsts; +using static Avalonia.OpenGL.Egl.EglConsts; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public class EglContext : IGlContext { private readonly EglDisplay _disp; private readonly EglInterface _egl; + private readonly EglContext _sharedWith; private readonly object _lock = new object(); - public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, EglSurface offscreenSurface, + public EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, Func offscreenSurface, GlVersion version, int sampleCount, int stencilSize) { _disp = display; _egl = egl; + _sharedWith = sharedWith; Context = ctx; - OffscreenSurface = offscreenSurface; + OffscreenSurface = offscreenSurface(this); Version = version; SampleCount = sampleCount; StencilSize = stencilSize; @@ -33,21 +35,17 @@ namespace Avalonia.OpenGL public int StencilSize { get; } public EglDisplay Display => _disp; - public IDisposable Lock() - { - Monitor.Enter(_lock); - return Disposable.Create(() => Monitor.Exit(_lock)); - } - class RestoreContext : IDisposable { private readonly EglInterface _egl; + private readonly object _l; private readonly IntPtr _display; private IntPtr _context, _read, _draw; - public RestoreContext(EglInterface egl, IntPtr defDisplay) + public RestoreContext(EglInterface egl, IntPtr defDisplay, object l) { _egl = egl; + _l = l; _display = _egl.GetCurrentDisplay(); if (_display == IntPtr.Zero) _display = defDisplay; @@ -59,29 +57,52 @@ namespace Avalonia.OpenGL public void Dispose() { _egl.MakeCurrent(_display, _draw, _read, _context); + Monitor.Exit(_l); } } - public IDisposable MakeCurrent() + public IDisposable MakeCurrent() => MakeCurrent(OffscreenSurface); + + public IDisposable MakeCurrent(EglSurface surface) { - var old = new RestoreContext(_egl, _disp.Handle); - _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - if (!_egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, Context)) - throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); - return old; + Monitor.Enter(_lock); + var success = false; + try + { + var old = new RestoreContext(_egl, _disp.Handle, _lock); + var surf = surface ?? OffscreenSurface; + _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context)) + throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); + success = true; + return old; + } + finally + { + if(!success) + Monitor.Enter(_lock); + } } - public IDisposable MakeCurrent(EglSurface surface) + public IDisposable EnsureCurrent() { - var old = new RestoreContext(_egl, _disp.Handle); - var surf = surface ?? OffscreenSurface; - _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context)) - throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); - return old; + if(IsCurrent) + return Disposable.Empty; + return MakeCurrent(); } + public bool IsSharedWith(IGlContext context) + { + var c = (EglContext)context; + return c == this + || c._sharedWith == this + || _sharedWith == context + || _sharedWith != null && _sharedWith == c._sharedWith; + } + + public bool IsCurrent => _egl.GetCurrentDisplay() == _disp.Handle && _egl.GetCurrentContext() == Context; + public void Dispose() { _egl.DestroyContext(_disp.Handle, Context); diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs similarity index 57% rename from src/Avalonia.OpenGL/EglDisplay.cs rename to src/Avalonia.OpenGL/Egl/EglDisplay.cs index 0436f6ac52..fd3de854f5 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -1,96 +1,104 @@ using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using Avalonia.Platform.Interop; -using static Avalonia.OpenGL.EglConsts; +using System.Linq; +using static Avalonia.OpenGL.Egl.EglConsts; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public class EglDisplay { private readonly EglInterface _egl; + public bool SupportsSharing { get; } private readonly IntPtr _display; private readonly IntPtr _config; private readonly int[] _contextAttributes; private readonly int _surfaceType; public IntPtr Handle => _display; - private AngleOptions.PlatformApi? _angleApi; + public IntPtr Config => _config; private int _sampleCount; private int _stencilSize; private GlVersion _version; - public EglDisplay(EglInterface egl) : this(egl, -1, IntPtr.Zero, null) + public EglDisplay(EglInterface egl, bool supportsSharing) : this(egl, supportsSharing, -1, IntPtr.Zero, null) { } - public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) - { - _egl = egl; + static IntPtr CreateDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) + { + var display = IntPtr.Zero; if (platformType == -1 && platformDisplay == IntPtr.Zero) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - if (_egl.GetPlatformDisplayEXT == null) - throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); - - var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new List {AngleOptions.PlatformApi.DirectX9}; - - foreach (var platformApi in allowedApis) - { - int dapi; - if (platformApi == AngleOptions.PlatformApi.DirectX9) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; - else if (platformApi == AngleOptions.PlatformApi.DirectX11) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; - else - continue; - - _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, - new[] {EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE}); - if (_display != IntPtr.Zero) - { - _angleApi = platformApi; - break; - } - } - - if (_display == IntPtr.Zero) - throw new OpenGlException("Unable to create ANGLE display"); - } - - if (_display == IntPtr.Zero) - _display = _egl.GetDisplay(IntPtr.Zero); + if (display == IntPtr.Zero) + display = egl.GetDisplay(IntPtr.Zero); } else { - if (_egl.GetPlatformDisplayEXT == null) + if (egl.GetPlatformDisplayEXT == null) throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); - _display = _egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs); + display = egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs); } + + if (display == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglGetDisplay", egl); + return display; + } - if (_display == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglGetDisplay", _egl); + public EglDisplay(EglInterface egl, bool supportsSharing, int platformType, IntPtr platformDisplay, int[] attrs) + : this(egl, supportsSharing, CreateDisplay(egl, platformType, platformDisplay, attrs)) + { + + } + public EglDisplay(EglInterface egl, bool supportsSharing, IntPtr display) + { + _egl = egl; + SupportsSharing = supportsSharing; + _display = display; + if(_display == IntPtr.Zero) + throw new ArgumentException(); + + if (!_egl.Initialize(_display, out var major, out var minor)) throw OpenGlException.GetFormattedException("eglInitialize", _egl); - foreach (var cfg in new[] + var glProfiles = AvaloniaLocator.Current.GetService()?.GlProfiles + ?? new[] + { + new GlVersion(GlProfileType.OpenGLES, 3, 0), + new GlVersion(GlProfileType.OpenGLES, 2, 0) + }; + + var cfgs = glProfiles.Select(x => { - new + var typeBit = EGL_OPENGL_ES3_BIT; + + switch (x.Major) + { + case 2: + typeBit = EGL_OPENGL_ES2_BIT; + break; + + case 1: + typeBit = EGL_OPENGL_ES_BIT; + break; + } + + return new { Attributes = new[] { - EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_CONTEXT_MAJOR_VERSION, x.Major, + EGL_CONTEXT_MINOR_VERSION, x.Minor, EGL_NONE }, Api = EGL_OPENGL_ES_API, - RenderableTypeBit = EGL_OPENGL_ES2_BIT, - Version = new GlVersion(GlProfileType.OpenGLES, 2, 0) - } - }) + RenderableTypeBit = typeBit, + Version = x + }; + }); + + foreach (var cfg in cfgs) { if (!_egl.BindApi(cfg.Api)) continue; @@ -128,7 +136,12 @@ namespace Avalonia.OpenGL throw new OpenGlException("No suitable EGL config was found"); } - public EglDisplay() : this(new EglInterface()) + public EglDisplay() : this(false) + { + + } + + public EglDisplay(bool supportsSharing) : this(new EglInterface(), supportsSharing) { } @@ -136,6 +149,9 @@ namespace Avalonia.OpenGL public EglInterface EglInterface => _egl; public EglContext CreateContext(IGlContext share) { + if (share != null && !SupportsSharing) + throw new NotSupportedException("Context sharing is not supported by this display"); + if((_surfaceType|EGL_PBUFFER_BIT) == 0) throw new InvalidOperationException("Platform doesn't support PBUFFER surfaces"); var shareCtx = (EglContext)share; @@ -150,27 +166,22 @@ namespace Avalonia.OpenGL }); if (surf == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); - var rv = new EglContext(this, _egl, ctx, new EglSurface(this, _egl, surf), + var rv = new EglContext(this, _egl, shareCtx, ctx, context => new EglSurface(this, context, surf), _version, _sampleCount, _stencilSize); return rv; } public EglContext CreateContext(EglContext share, EglSurface offscreenSurface) { + if (share != null && !SupportsSharing) + throw new NotSupportedException("Context sharing is not supported by this display"); + var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes); if (ctx == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglCreateContext", _egl); - var rv = new EglContext(this, _egl, ctx, offscreenSurface, _version, _sampleCount, _stencilSize); + var rv = new EglContext(this, _egl, share, ctx, _ => offscreenSurface, _version, _sampleCount, _stencilSize); rv.MakeCurrent(null); return rv; } - - public EglSurface CreateWindowSurface(IntPtr window) - { - var s = _egl.CreateWindowSurface(_display, _config, window, new[] {EGL_NONE, EGL_NONE}); - if (s == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateWindowSurface", _egl); - return new EglSurface(this, _egl, s); - } } } diff --git a/src/Avalonia.OpenGL/EglErrors.cs b/src/Avalonia.OpenGL/Egl/EglErrors.cs similarity index 96% rename from src/Avalonia.OpenGL/EglErrors.cs rename to src/Avalonia.OpenGL/Egl/EglErrors.cs index bfe46f2b69..d89bbb499f 100644 --- a/src/Avalonia.OpenGL/EglErrors.cs +++ b/src/Avalonia.OpenGL/Egl/EglErrors.cs @@ -1,4 +1,4 @@ -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public enum EglErrors { diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs new file mode 100644 index 0000000000..3d58660d47 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs @@ -0,0 +1,54 @@ +using Avalonia.OpenGL.Surfaces; + +namespace Avalonia.OpenGL.Egl +{ + public class EglGlPlatformSurface : EglGlPlatformSurfaceBase + { + private readonly EglPlatformOpenGlInterface _egl; + private readonly IEglWindowGlPlatformSurfaceInfo _info; + + public EglGlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info) : base() + { + _egl = egl; + _info = info; + } + + public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + { + var glSurface = _egl.CreateWindowSurface(_info.Handle); + return new RenderTarget(_egl, glSurface, _info); + } + + class RenderTarget : EglPlatformSurfaceRenderTargetBase + { + private readonly EglPlatformOpenGlInterface _egl; + private EglSurface _glSurface; + private readonly IEglWindowGlPlatformSurfaceInfo _info; + private PixelSize _currentSize; + + public RenderTarget(EglPlatformOpenGlInterface egl, + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) : base(egl) + { + _egl = egl; + _glSurface = glSurface; + _info = info; + _currentSize = info.Size; + } + + public override void Dispose() => _glSurface.Dispose(); + + public override IGlPlatformSurfaceRenderingSession BeginDraw() + { + if (_info.Size != _currentSize || _glSurface == null) + { + _glSurface?.Dispose(); + _glSurface = null; + _glSurface = _egl.CreateWindowSurface(_info.Handle); + _currentSize = _info.Size; + } + return base.BeginDraw(_glSurface, _info); + } + } + } +} + diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs new file mode 100644 index 0000000000..4ea6766de2 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs @@ -0,0 +1,100 @@ +using System; +using Avalonia.OpenGL.Surfaces; + +namespace Avalonia.OpenGL.Egl +{ + public abstract class EglGlPlatformSurfaceBase : IGlPlatformSurface + { + public interface IEglWindowGlPlatformSurfaceInfo + { + IntPtr Handle { get; } + PixelSize Size { get; } + double Scaling { get; } + } + + public abstract IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(); + } + + public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTarget + { + private readonly EglPlatformOpenGlInterface _egl; + + protected EglPlatformSurfaceRenderTargetBase(EglPlatformOpenGlInterface egl) + { + _egl = egl; + } + + public virtual void Dispose() + { + + } + + public abstract IGlPlatformSurfaceRenderingSession BeginDraw(); + + protected IGlPlatformSurfaceRenderingSession BeginDraw(EglSurface surface, + EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, Action onFinish = null, bool isYFlipped = false) + { + + var restoreContext = _egl.PrimaryEglContext.MakeCurrent(surface); + var success = false; + try + { + var egli = _egl.Display.EglInterface; + egli.WaitClient(); + egli.WaitGL(); + egli.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); + + _egl.PrimaryContext.GlInterface.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); + + success = true; + return new Session(_egl.Display, _egl.PrimaryEglContext, surface, info, restoreContext, onFinish, isYFlipped); + } + finally + { + if(!success) + restoreContext.Dispose(); + } + } + + class Session : IGlPlatformSurfaceRenderingSession + { + private readonly EglContext _context; + private readonly EglSurface _glSurface; + private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; + private readonly EglDisplay _display; + private readonly IDisposable _restoreContext; + private readonly Action _onFinish; + + + public Session(EglDisplay display, EglContext context, + EglSurface glSurface, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, + IDisposable restoreContext, Action onFinish, bool isYFlipped) + { + IsYFlipped = isYFlipped; + _context = context; + _display = display; + _glSurface = glSurface; + _info = info; + _restoreContext = restoreContext; + _onFinish = onFinish; + } + + public void Dispose() + { + _context.GlInterface.Flush(); + _display.EglInterface.WaitGL(); + _glSurface.SwapBuffers(); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); + _restoreContext.Dispose(); + _onFinish?.Invoke(); + } + + public IGlContext Context => _context; + public PixelSize Size => _info.Size; + public double Scaling => _info.Scaling; + public bool IsYFlipped { get; } + } + } +} diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs similarity index 85% rename from src/Avalonia.OpenGL/EglInterface.cs rename to src/Avalonia.OpenGL/Egl/EglInterface.cs index c0665a1ea1..8055226042 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; using Avalonia.Platform; using Avalonia.Platform.Interop; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public class EglInterface : GlInterfaceBase { @@ -17,25 +17,21 @@ namespace Avalonia.OpenGL } + public EglInterface(Func getProcAddress) : base(getProcAddress) + { + + } + public EglInterface(string library) : base(Load(library)) { } - [DllImport("libegl.dll", CharSet = CharSet.Ansi)] - static extern IntPtr eglGetProcAddress(string proc); static Func Load() { var os = AvaloniaLocator.Current.GetService().GetRuntimeInfo().OperatingSystem; if(os == OperatingSystemType.Linux || os == OperatingSystemType.Android) return Load("libEGL.so.1"); - if (os == OperatingSystemType.WinNT) - { - var disp = eglGetProcAddress("eglGetPlatformDisplayEXT"); - if (disp == IntPtr.Zero) - throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point"); - return eglGetProcAddress; - } throw new PlatformNotSupportedException(); } @@ -147,6 +143,21 @@ namespace Avalonia.OpenGL return null; return Marshal.PtrToStringAnsi(rv); } + + public delegate IntPtr EglCreatePbufferFromClientBuffer(IntPtr display, int buftype, IntPtr buffer, IntPtr config, int[] attrib_list); + [GlEntryPoint("eglCreatePbufferFromClientBuffer")] + + public EglCreatePbufferFromClientBuffer CreatePbufferFromClientBuffer { get; } + + public delegate bool EglQueryDisplayAttribEXT(IntPtr display, int attr, out IntPtr res); + + [GlEntryPoint("eglQueryDisplayAttribEXT"), GlOptionalEntryPoint] + public EglQueryDisplayAttribEXT QueryDisplayAttribExt { get; } + + public delegate bool EglQueryDeviceAttribEXT(IntPtr display, int attr, out IntPtr res); + + [GlEntryPoint("eglQueryDeviceAttribEXT"), GlOptionalEntryPoint] + public EglQueryDisplayAttribEXT QueryDeviceAttribExt { get; } // ReSharper restore UnassignedGetOnlyAutoProperty } diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs new file mode 100644 index 0000000000..476f65a774 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs @@ -0,0 +1,72 @@ +using System; +using Avalonia.Logging; +using static Avalonia.OpenGL.Egl.EglConsts; + +namespace Avalonia.OpenGL.Egl +{ + public class EglPlatformOpenGlInterface : IPlatformOpenGlInterface + { + public EglDisplay Display { get; private set; } + public bool CanCreateContexts => true; + public bool CanShareContexts => Display.SupportsSharing; + + public EglContext PrimaryEglContext { get; } + public IGlContext PrimaryContext => PrimaryEglContext; + + public EglPlatformOpenGlInterface(EglDisplay display) + { + Display = display; + PrimaryEglContext = display.CreateContext(null); + } + + public static void TryInitialize() + { + var feature = TryCreate(); + if (feature != null) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + } + + public static EglPlatformOpenGlInterface TryCreate() => TryCreate(() => new EglDisplay()); + public static EglPlatformOpenGlInterface TryCreate(Func displayFactory) + { + try + { + return new EglPlatformOpenGlInterface(displayFactory()); + } + catch(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); + return null; + } + } + + public IGlContext CreateContext() => Display.CreateContext(null); + public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryEglContext); + + + public EglSurface CreateWindowSurface(IntPtr window) + { + using (PrimaryContext.MakeCurrent()) + { + var s = Display.EglInterface.CreateWindowSurface(Display.Handle, Display.Config, window, + new[] { EGL_NONE, EGL_NONE }); + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateWindowSurface", Display.EglInterface); + return new EglSurface(Display, PrimaryEglContext, s); + } + } + + public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) + { + using (PrimaryContext.MakeCurrent()) + { + var s = Display.EglInterface.CreatePbufferFromClientBuffer(Display.Handle, bufferType, handle, + Display.Config, attribs); + + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", Display.EglInterface); + return new EglSurface(Display, PrimaryEglContext, s); + } + } + } +} diff --git a/src/Avalonia.OpenGL/EglSurface.cs b/src/Avalonia.OpenGL/Egl/EglSurface.cs similarity index 57% rename from src/Avalonia.OpenGL/EglSurface.cs rename to src/Avalonia.OpenGL/Egl/EglSurface.cs index 5ac56a00e3..a93751ca9e 100644 --- a/src/Avalonia.OpenGL/EglSurface.cs +++ b/src/Avalonia.OpenGL/Egl/EglSurface.cs @@ -1,22 +1,25 @@ using System; using System.Runtime.InteropServices; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Egl { public class EglSurface : SafeHandle { private readonly EglDisplay _display; + private readonly EglContext _context; private readonly EglInterface _egl; - public EglSurface(EglDisplay display, EglInterface egl, IntPtr surface) : base(surface, true) + public EglSurface(EglDisplay display, EglContext context, IntPtr surface) : base(surface, true) { _display = display; - _egl = egl; + _context = context; + _egl = display.EglInterface; } protected override bool ReleaseHandle() { - _egl.DestroySurface(_display.Handle, handle); + using (_context.MakeCurrent()) + _egl.DestroySurface(_display.Handle, handle); return true; } diff --git a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs deleted file mode 100644 index f59c6b7751..0000000000 --- a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Avalonia.Logging; - -namespace Avalonia.OpenGL -{ - public class EglGlPlatformFeature : IWindowingPlatformGlFeature - { - private EglDisplay _display; - public EglDisplay Display => _display; - public IGlContext CreateContext() - { - return _display.CreateContext(DeferredContext); - } - public EglContext DeferredContext { get; private set; } - public IGlContext MainContext => DeferredContext; - - public static void TryInitialize() - { - var feature = TryCreate(); - if (feature != null) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); - } - - public static EglGlPlatformFeature TryCreate() - { - try - { - var disp = new EglDisplay(); - return new EglGlPlatformFeature - { - _display = disp, - DeferredContext = disp.CreateContext(null) - }; - } - catch(Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); - return null; - } - } - } -} diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs deleted file mode 100644 index 3e4befe2c6..0000000000 --- a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.Threading; - -namespace Avalonia.OpenGL -{ - public class EglGlPlatformSurface : IGlPlatformSurface - { - public interface IEglWindowGlPlatformSurfaceInfo - { - IntPtr Handle { get; } - PixelSize Size { get; } - double Scaling { get; } - } - - private readonly EglDisplay _display; - private readonly EglContext _context; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - - public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info) - { - _display = context.Display; - _context = context; - _info = info; - } - - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() - { - var glSurface = _display.CreateWindowSurface(_info.Handle); - return new RenderTarget(_display, _context, glSurface, _info); - } - - class RenderTarget : IGlPlatformSurfaceRenderTargetWithCorruptionInfo - { - private readonly EglDisplay _display; - private readonly EglContext _context; - private readonly EglSurface _glSurface; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - private PixelSize _initialSize; - - public RenderTarget(EglDisplay display, EglContext context, - EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) - { - _display = display; - _context = context; - _glSurface = glSurface; - _info = info; - _initialSize = info.Size; - } - - public void Dispose() => _glSurface.Dispose(); - - public bool IsCorrupted => _initialSize != _info.Size; - - public IGlPlatformSurfaceRenderingSession BeginDraw() - { - var l = _context.Lock(); - try - { - if (IsCorrupted) - throw new RenderTargetCorruptedException(); - var restoreContext = _context.MakeCurrent(_glSurface); - _display.EglInterface.WaitClient(); - _display.EglInterface.WaitGL(); - _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); - - return new Session(_display, _context, _glSurface, _info, l, restoreContext); - } - catch - { - l.Dispose(); - throw; - } - } - - class Session : IGlPlatformSurfaceRenderingSession - { - private readonly EglContext _context; - private readonly EglSurface _glSurface; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - private readonly EglDisplay _display; - private IDisposable _lock; - private readonly IDisposable _restoreContext; - - - public Session(EglDisplay display, EglContext context, - EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info, - IDisposable @lock, IDisposable restoreContext) - { - _context = context; - _display = display; - _glSurface = glSurface; - _info = info; - _lock = @lock; - _restoreContext = restoreContext; - } - - public void Dispose() - { - _context.GlInterface.Flush(); - _display.EglInterface.WaitGL(); - _glSurface.SwapBuffers(); - _display.EglInterface.WaitClient(); - _display.EglInterface.WaitGL(); - _display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); - _restoreContext.Dispose(); - _lock.Dispose(); - } - - public IGlContext Context => _context; - public PixelSize Size => _info.Size; - public double Scaling => _info.Scaling; - public bool IsYFlipped { get; } - } - } - } -} - diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index 23188e7dbf..ea2fe0a99c 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -82,6 +82,9 @@ namespace Avalonia.OpenGL [GlEntryPoint("glFlush")] public Action Flush { get; } + + [GlEntryPoint("glFinish")] + public Action Finish { get; } public delegate IntPtr GlGetString(int v); [GlEntryPoint("glGetString")] @@ -144,6 +147,10 @@ namespace Avalonia.OpenGL [GlEntryPoint("glBindTexture")] public GlBindTexture BindTexture { get; } + public delegate void GlActiveTexture(int texture); + [GlEntryPoint("glActiveTexture")] + public GlActiveTexture ActiveTexture { get; } + public delegate void GlDeleteTextures(int count, int[] textures); [GlEntryPoint("glDeleteTextures")] public GlDeleteTextures DeleteTextures { get; } @@ -154,6 +161,12 @@ namespace Avalonia.OpenGL [GlEntryPoint("glTexImage2D")] public GlTexImage2D TexImage2D { get; } + public delegate void GlCopyTexSubImage2D(int target, int level, int xoffset, int yoffset, int x, int y, + int width, int height); + + [GlEntryPoint("glCopyTexSubImage2D")] + public GlCopyTexSubImage2D CopyTexSubImage2D { get; } + public delegate void GlTexParameteri(int target, int name, int value); [GlEntryPoint("glTexParameteri")] public GlTexParameteri TexParameteri { get; } diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index eb4313fba9..50868db873 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -9,5 +9,7 @@ namespace Avalonia.OpenGL int SampleCount { get; } int StencilSize { get; } IDisposable MakeCurrent(); + IDisposable EnsureCurrent(); + bool IsSharedWith(IGlContext context); } } diff --git a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs b/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs index 30f83745ad..fdb9162164 100644 --- a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs +++ b/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs @@ -4,6 +4,6 @@ namespace Avalonia.OpenGL { public interface IOpenGlAwarePlatformRenderInterface { - IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap(); + IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); } } diff --git a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs new file mode 100644 index 0000000000..5ee5df1e85 --- /dev/null +++ b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs @@ -0,0 +1,13 @@ +namespace Avalonia.OpenGL +{ + public interface IPlatformOpenGlInterface + { + IGlContext PrimaryContext { get; } + IGlContext CreateSharedContext(); + bool CanShareContexts { get; } + bool CanCreateContexts { get; } + IGlContext CreateContext(); + /*IGlContext TryCreateContext(GlVersion version); + */ + } +} diff --git a/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs b/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs deleted file mode 100644 index b91496f42b..0000000000 --- a/src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Avalonia.OpenGL -{ - public interface IWindowingPlatformGlFeature - { - IGlContext CreateContext(); - IGlContext MainContext { get; } - } -} diff --git a/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs b/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs new file mode 100644 index 0000000000..aef4f601be --- /dev/null +++ b/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs @@ -0,0 +1,17 @@ +using System; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace Avalonia.OpenGL.Imaging +{ + public interface IOpenGlBitmapImpl : IBitmapImpl + { + IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback); + bool SupportsContext(IGlContext context); + } + + public interface IOpenGlBitmapAttachment : IDisposable + { + void Present(); + } +} diff --git a/src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs b/src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs deleted file mode 100644 index e5f3691569..0000000000 --- a/src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using Avalonia.Media.Imaging; -using Avalonia.Platform; - -namespace Avalonia.OpenGL.Imaging -{ - public interface IOpenGlTextureBitmapImpl : IBitmapImpl - { - IDisposable Lock(); - void SetBackBuffer(int textureId, int internalFormat, PixelSize pixelSize, double dpiScaling); - void SetDirty(); - } -} diff --git a/src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs similarity index 54% rename from src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs rename to src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs index 558eae8fdf..7af44cd624 100644 --- a/src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs +++ b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs @@ -6,32 +6,30 @@ using Avalonia.Threading; namespace Avalonia.OpenGL.Imaging { - public class OpenGlTextureBitmap : Bitmap, IAffectsRender + public class OpenGlBitmap : Bitmap, IAffectsRender { - private IOpenGlTextureBitmapImpl _impl; - static IOpenGlTextureBitmapImpl CreateOrThrow() + private IOpenGlBitmapImpl _impl; + + public OpenGlBitmap(PixelSize size, Vector dpi) + : base(CreateOrThrow(size, dpi)) { - if (!(AvaloniaLocator.Current.GetService() is IOpenGlAwarePlatformRenderInterface - glAware)) - throw new PlatformNotSupportedException("Rendering platform does not support OpenGL integration"); - return glAware.CreateOpenGlTextureBitmap(); + _impl = (IOpenGlBitmapImpl)PlatformImpl.Item; } - public OpenGlTextureBitmap() - : base(CreateOrThrow()) + static IOpenGlBitmapImpl CreateOrThrow(PixelSize size, Vector dpi) { - _impl = (IOpenGlTextureBitmapImpl)PlatformImpl.Item; + if (!(AvaloniaLocator.Current.GetService() is IOpenGlAwarePlatformRenderInterface + glAware)) + throw new PlatformNotSupportedException("Rendering platform does not support OpenGL integration"); + return glAware.CreateOpenGlBitmap(size, dpi); } - public IDisposable Lock() => _impl.Lock(); + public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context) => + _impl.CreateFramebufferAttachment(context, SetIsDirty); - public void SetTexture(int textureId, int internalFormat, PixelSize size, double dpiScaling) - { - _impl.SetBackBuffer(textureId, internalFormat, size, dpiScaling); - SetIsDirty(); - } - - public void SetIsDirty() + public bool SupportsContext(IGlContext context) => _impl.SupportsContext(context); + + void SetIsDirty() { if (Dispatcher.UIThread.CheckAccess()) CallInvalidated(); diff --git a/src/Avalonia.OpenGL/OpenGlException.cs b/src/Avalonia.OpenGL/OpenGlException.cs index d3cd7d059e..196f507ad8 100644 --- a/src/Avalonia.OpenGL/OpenGlException.cs +++ b/src/Avalonia.OpenGL/OpenGlException.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.OpenGL.Egl; namespace Avalonia.OpenGL { diff --git a/src/Avalonia.OpenGL/IGlPlatformSurface.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs similarity index 77% rename from src/Avalonia.OpenGL/IGlPlatformSurface.cs rename to src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs index 22d36b4472..875c215336 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs @@ -1,4 +1,4 @@ -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Surfaces { public interface IGlPlatformSurface { diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs similarity index 89% rename from src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs rename to src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs index d198d46e5c..f89b6f04f5 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs @@ -1,6 +1,6 @@ using System; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Surfaces { public interface IGlPlatformSurfaceRenderTarget : IDisposable { diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderingSession.cs similarity index 86% rename from src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs rename to src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderingSession.cs index 89911a20a8..da06eab1e7 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderingSession.cs @@ -1,6 +1,6 @@ using System; -namespace Avalonia.OpenGL +namespace Avalonia.OpenGL.Surfaces { public interface IGlPlatformSurfaceRenderingSession : IDisposable { diff --git a/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj b/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj new file mode 100644 index 0000000000..75eeb92f42 --- /dev/null +++ b/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj @@ -0,0 +1,12 @@ + + + netstandard2.0 + Avalonia.ReactiveUI.Events + + + + + + + + diff --git a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs new file mode 100644 index 0000000000..0060767565 --- /dev/null +++ b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +#nullable enable + +namespace Avalonia.Controls.Metadata +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class PseudoClassesAttribute : Attribute + { + public PseudoClassesAttribute(params string[] pseudoClasses) + { + PseudoClasses = pseudoClasses; + } + + public IReadOnlyList PseudoClasses { get; } + } +} diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index 44dfb9ea48..5f18bac44a 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -72,5 +72,8 @@ 18 8 + + 20 + 20 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 9ed3207235..30c6d39856 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -75,5 +75,8 @@ 18 8 + + 20 + 20 diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index e5b654b490..625b409598 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -12,7 +12,7 @@ - + @@ -21,6 +21,7 @@ + diff --git a/src/Avalonia.Themes.Default/PathIcon.xaml b/src/Avalonia.Themes.Default/PathIcon.xaml new file mode 100644 index 0000000000..a2d01f7b5b --- /dev/null +++ b/src/Avalonia.Themes.Default/PathIcon.xaml @@ -0,0 +1,18 @@ + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 46488c1c57..134e804c53 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -20,5 +20,7 @@ 1 2 10,6,6,5 + 20 + 20 diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml index e58e8758d2..8522c933ae 100644 --- a/src/Avalonia.Themes.Fluent/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -77,7 +77,7 @@ - - diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index 5f82fed08c..73c44c02ab 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -8,11 +8,11 @@ - + - + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 00ab9bc375..90c0f55b68 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -11,7 +11,7 @@ - + @@ -20,6 +20,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/PathIcon.xaml b/src/Avalonia.Themes.Fluent/PathIcon.xaml new file mode 100644 index 0000000000..0a2c747514 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/PathIcon.xaml @@ -0,0 +1,26 @@ + + + + + M14 9.50006C11.5147 9.50006 9.5 11.5148 9.5 14.0001C9.5 16.4853 11.5147 18.5001 14 18.5001C15.3488 18.5001 16.559 17.9066 17.3838 16.9666C18.0787 16.1746 18.5 15.1365 18.5 14.0001C18.5 13.5401 18.431 13.0963 18.3028 12.6784C17.7382 10.8381 16.0253 9.50006 14 9.50006ZM11 14.0001C11 12.3432 12.3431 11.0001 14 11.0001C15.6569 11.0001 17 12.3432 17 14.0001C17 15.6569 15.6569 17.0001 14 17.0001C12.3431 17.0001 11 15.6569 11 14.0001Z M21.7093 22.3948L19.9818 21.6364C19.4876 21.4197 18.9071 21.4515 18.44 21.7219C17.9729 21.9924 17.675 22.4693 17.6157 23.0066L17.408 24.8855C17.3651 25.273 17.084 25.5917 16.7055 25.682C14.9263 26.1061 13.0725 26.1061 11.2933 25.682C10.9148 25.5917 10.6336 25.273 10.5908 24.8855L10.3834 23.0093C10.3225 22.4731 10.0112 21.9976 9.54452 21.7281C9.07783 21.4586 8.51117 21.4269 8.01859 21.6424L6.29071 22.4009C5.93281 22.558 5.51493 22.4718 5.24806 22.1859C4.00474 20.8536 3.07924 19.2561 2.54122 17.5137C2.42533 17.1384 2.55922 16.7307 2.8749 16.4977L4.40219 15.3703C4.83721 15.0501 5.09414 14.5415 5.09414 14.0007C5.09414 13.4598 4.83721 12.9512 4.40162 12.6306L2.87529 11.5051C2.55914 11.272 2.42513 10.8638 2.54142 10.4882C3.08038 8.74734 4.00637 7.15163 5.24971 5.82114C5.51684 5.53528 5.93492 5.44941 6.29276 5.60691L8.01296 6.36404C8.50793 6.58168 9.07696 6.54881 9.54617 6.27415C10.0133 6.00264 10.3244 5.52527 10.3844 4.98794L10.5933 3.11017C10.637 2.71803 10.9245 2.39704 11.3089 2.31138C12.19 2.11504 13.0891 2.01071 14.0131 2.00006C14.9147 2.01047 15.8128 2.11485 16.6928 2.31149C17.077 2.39734 17.3643 2.71823 17.4079 3.11017L17.617 4.98937C17.7116 5.85221 18.4387 6.50572 19.3055 6.50663C19.5385 6.507 19.769 6.45838 19.9843 6.36294L21.7048 5.60568C22.0626 5.44818 22.4807 5.53405 22.7478 5.81991C23.9912 7.1504 24.9172 8.74611 25.4561 10.487C25.5723 10.8623 25.4386 11.2703 25.1228 11.5035L23.5978 12.6297C23.1628 12.95 22.9 13.4586 22.9 13.9994C22.9 14.5403 23.1628 15.0489 23.5988 15.3698L25.1251 16.4965C25.441 16.7296 25.5748 17.1376 25.4586 17.5131C24.9198 19.2536 23.9944 20.8492 22.7517 22.1799C22.4849 22.4657 22.0671 22.5518 21.7093 22.3948ZM16.263 22.1966C16.4982 21.4685 16.9889 20.8288 17.6884 20.4238C18.5702 19.9132 19.6536 19.8547 20.5841 20.2627L21.9281 20.8526C22.791 19.8538 23.4593 18.7013 23.8981 17.4552L22.7095 16.5778L22.7086 16.5771C21.898 15.98 21.4 15.0277 21.4 13.9994C21.4 12.9719 21.8974 12.0195 22.7073 11.4227L22.7085 11.4218L23.8957 10.545C23.4567 9.2988 22.7881 8.14636 21.9248 7.1477L20.5922 7.73425L20.5899 7.73527C20.1844 7.91463 19.7472 8.00722 19.3039 8.00663C17.6715 8.00453 16.3046 6.77431 16.1261 5.15465L16.1259 5.15291L15.9635 3.69304C15.3202 3.57328 14.6677 3.50872 14.013 3.50017C13.3389 3.50891 12.6821 3.57367 12.0377 3.69328L11.8751 5.15452C11.7625 6.16272 11.1793 7.05909 10.3019 7.56986C9.41937 8.0856 8.34453 8.14844 7.40869 7.73694L6.07273 7.14893C5.20949 8.14751 4.54092 9.29983 4.10196 10.5459L5.29181 11.4233C6.11115 12.0269 6.59414 12.9837 6.59414 14.0007C6.59414 15.0173 6.11142 15.9742 5.29237 16.5776L4.10161 17.4566C4.54002 18.7044 5.2085 19.8585 6.07205 20.8587L7.41742 20.2682C8.34745 19.8613 9.41573 19.9215 10.2947 20.4292C11.174 20.937 11.7593 21.832 11.8738 22.84L11.8744 22.8445L12.0362 24.3088C13.3326 24.5638 14.6662 24.5638 15.9626 24.3088L16.1247 22.8418C16.1491 22.6217 16.1955 22.4055 16.263 22.1966Z + + + + + + diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml index acde4ea0be..078f51c87a 100644 --- a/src/Avalonia.Themes.Fluent/RadioButton.xaml +++ b/src/Avalonia.Themes.Fluent/RadioButton.xaml @@ -13,11 +13,11 @@ - + - + diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index 5058cff26d..148916932f 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -1,5 +1,33 @@ Compat issues with assembly Avalonia.Visuals: +MembersMustExist : Member 'public void Avalonia.Media.DrawingContext.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.GetOrAddTypeface(Avalonia.Media.FontFamily, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.MatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.GlyphRun.Bounds.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Media.GlyphRunDrawing.BaselineOriginProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Point Avalonia.Media.GlyphRunDrawing.BaselineOrigin.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.GlyphRunDrawing.BaselineOrigin.set(Avalonia.Point)' does not exist in the implementation but it does exist in the contract. +CannotSealType : Type 'Avalonia.Media.Typeface' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +TypeCannotChangeClassification : Type 'Avalonia.Media.Typeface' is a 'struct' in the implementation but is a 'class' in the contract. +CannotMakeMemberNonVirtual : Member 'public System.Boolean Avalonia.Media.Typeface.Equals(System.Object)' is non-virtual in the implementation but is virtual in the contract. +CannotMakeMemberNonVirtual : Member 'public System.Int32 Avalonia.Media.Typeface.GetHashCode()' is non-virtual in the implementation but is virtual in the contract. +TypesMustExist : Type 'Avalonia.Media.Fonts.FontKey' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.DrawableTextRun.Size' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.TextFormatting.DrawableTextRun.Bounds.get()' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.DrawableTextRun.Size.get()' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.TextFormatting.ShapedTextCharacters.Bounds.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak' is abstract in the implementation but is missing in the contract. +CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext)' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.LineBreak.get()' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak.get()' is abstract in the implementation but is missing in the contract. -Total Issues: 3 +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' is present in the implementation but not in the contract. +Total Issues: 31 diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index ba7191d7a6..ae4c927ae2 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -206,14 +206,13 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - /// The baseline origin of the glyph run. - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { Contract.Requires(glyphRun != null); if (foreground != null) { - PlatformImpl.DrawGlyphRun(foreground, glyphRun, baselineOrigin); + PlatformImpl.DrawGlyphRun(foreground, glyphRun); } } diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index ad3fee7eb7..db87c7c6c4 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -13,8 +13,8 @@ namespace Avalonia.Media /// public sealed class FontManager { - private readonly ConcurrentDictionary _typefaceCache = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); private readonly FontFamily _defaultFontFamily; public FontManager(IFontManagerImpl platformImpl) @@ -76,79 +76,52 @@ namespace Avalonia.Media PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); /// - /// Returns a new typeface, or an existing one if a matching typeface exists. + /// Returns a new , or an existing one if a matching exists. /// - /// The font family. - /// The font style. - /// The font weight. + /// The typeface. /// - /// The typeface. + /// The . /// - public Typeface GetOrAddTypeface(FontFamily fontFamily, FontStyle fontStyle = FontStyle.Normal, - FontWeight fontWeight = FontWeight.Normal) + public GlyphTypeface GetOrAddGlyphTypeface(Typeface typeface) { while (true) { - if (fontFamily.IsDefault) + if (_glyphTypefaceCache.TryGetValue(typeface, out var glyphTypeface)) { - fontFamily = _defaultFontFamily; + return glyphTypeface; } - var key = new FontKey(fontFamily.Name, fontStyle, fontWeight); + glyphTypeface = new GlyphTypeface(typeface); - if (_typefaceCache.TryGetValue(key, out var typeface)) + if (_glyphTypefaceCache.TryAdd(typeface, glyphTypeface)) { - return typeface; + return glyphTypeface; } - typeface = new Typeface(fontFamily, fontStyle, fontWeight); - - if (_typefaceCache.TryAdd(key, typeface)) - { - return typeface; - } - - if (fontFamily == _defaultFontFamily) + if (typeface.FontFamily == _defaultFontFamily) { return null; } - fontFamily = _defaultFontFamily; + typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight); } } /// - /// Tries to match a specified character to a typeface that supports specified font properties. - /// Returns null if no fallback was found. + /// Tries to match a specified character to a that supports specified font properties. /// /// The codepoint to match against. /// The font style. /// The font weight. /// The font family. This is optional and used for fallback lookup. /// The culture. + /// The matching . /// - /// The matched typeface. + /// True, if the could match the character to specified parameters, False otherwise. /// - public Typeface MatchCharacter(int codepoint, - FontStyle fontStyle = FontStyle.Normal, - FontWeight fontWeight = FontWeight.Normal, - FontFamily fontFamily = null, CultureInfo culture = null) - { - foreach (var cachedTypeface in _typefaceCache.Values) - { - // First try to find a cached typeface by style and weight to avoid redundant glyph index lookup. - if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight - && cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0) - { - return cachedTypeface; - } - } - - var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out var key) ? - _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Style, key.Weight)) : - null; - - return matchedTypeface; - } + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, + FontFamily fontFamily, CultureInfo culture, out Typeface typeface) => + PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out typeface); } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs deleted file mode 100644 index b330db8462..0000000000 --- a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; - -namespace Avalonia.Media.Fonts -{ - public readonly struct FontKey : IEquatable - { - public FontKey(string familyName, FontStyle style, FontWeight weight) - { - FamilyName = familyName; - Style = style; - Weight = weight; - } - - public string FamilyName { get; } - public FontStyle Style { get; } - public FontWeight Weight { get; } - - public override int GetHashCode() - { - var hash = FamilyName.GetHashCode(); - - hash = hash * 31 + (int)Style; - hash = hash * 31 + (int)Weight; - - return hash; - } - - public override bool Equals(object other) - { - return other is FontKey key && Equals(key); - } - - public bool Equals(FontKey other) - { - return FamilyName == other.FamilyName && - Style == other.Style && - Weight == other.Weight; - } - } -} diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index da3a1f721c..14ab083b4f 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -16,8 +16,9 @@ namespace Avalonia.Media private IGlyphRunImpl _glyphRunImpl; private GlyphTypeface _glyphTypeface; private double _fontRenderingEmSize; - private Rect? _bounds; + private Size? _size; private int _biDiLevel; + private Point? _baselineOrigin; private ReadOnlySlice _glyphIndices; private ReadOnlySlice _glyphAdvances; @@ -89,6 +90,20 @@ namespace Avalonia.Media set => Set(ref _fontRenderingEmSize, value); } + /// + /// Gets or sets the baseline origin of the. + /// + public Point BaselineOrigin + { + get + { + _baselineOrigin ??= CalculateBaselineOrigin(); + + return _baselineOrigin.Value; + } + set => Set(ref _baselineOrigin, value); + } + /// /// Gets or sets an array of values that represent the glyph indices in the rendering physical font. /// @@ -156,16 +171,13 @@ namespace Avalonia.Media /// /// Gets or sets the conservative bounding box of the . /// - public Rect Bounds + public Size Size { get { - if (_bounds == null) - { - _bounds = CalculateBounds(); - } + _size ??= CalculateSize(); - return _bounds.Value; + return _size.Value; } } @@ -200,7 +212,7 @@ namespace Avalonia.Media if (characterHit.FirstCharacterIndex + characterHit.TrailingLength > Characters.End) { - return Bounds.Width; + return Size.Width; } var glyphIndex = FindGlyphIndex(characterHit.FirstCharacterIndex); @@ -257,7 +269,7 @@ namespace Avalonia.Media } //After - if (distance > Bounds.Size.Width) + if (distance > Size.Width) { isInside = false; @@ -529,12 +541,21 @@ namespace Avalonia.Media } /// - /// Calculates the bounds of the . + /// Calculates the default baseline origin of the . + /// + /// The baseline origin. + private Point CalculateBaselineOrigin() + { + return new Point(0, -GlyphTypeface.Ascent * Scale); + } + + /// + /// Calculates the size of the . /// /// /// The calculated bounds. /// - private Rect CalculateBounds() + private Size CalculateSize() { var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; @@ -555,7 +576,7 @@ namespace Avalonia.Media } } - return new Rect(0, GlyphTypeface.Ascent * Scale, width, height); + return new Size(width, height); } private void Set(ref T field, T value) @@ -590,11 +611,15 @@ namespace Avalonia.Media throw new InvalidOperationException(); } + _baselineOrigin = new Point(0, -GlyphTypeface.Ascent * Scale); + var platformRenderInterface = AvaloniaLocator.Current.GetService(); _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width); var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; + + _size = new Size(width, height); } void IDisposable.Dispose() diff --git a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs index d0ea113a6f..7e0d5c3c81 100644 --- a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs @@ -8,9 +8,6 @@ public static readonly StyledProperty GlyphRunProperty = AvaloniaProperty.Register(nameof(GlyphRun)); - public static readonly StyledProperty BaselineOriginProperty = - AvaloniaProperty.Register(nameof(BaselineOrigin)); - public IBrush Foreground { get => GetValue(ForegroundProperty); @@ -23,12 +20,6 @@ set => SetValue(GlyphRunProperty, value); } - public Point BaselineOrigin - { - get => GetValue(BaselineOriginProperty); - set => SetValue(BaselineOriginProperty, value); - } - public override void Draw(DrawingContext context) { if (GlyphRun == null) @@ -36,12 +27,12 @@ return; } - context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + context.DrawGlyphRun(Foreground, GlyphRun); } public override Rect GetBounds() { - return GlyphRun?.Bounds ?? default; + return GlyphRun != null ? new Rect(GlyphRun.Size) : Rect.Empty; } } } diff --git a/src/Avalonia.Visuals/Media/TextDecoration.cs b/src/Avalonia.Visuals/Media/TextDecoration.cs index 681fc5d499..d9b3f664ce 100644 --- a/src/Avalonia.Visuals/Media/TextDecoration.cs +++ b/src/Avalonia.Visuals/Media/TextDecoration.cs @@ -155,8 +155,7 @@ namespace Avalonia.Media /// /// The drawing context. /// The shaped characters that are decorated. - /// The origin. - internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTextCharacters, Point origin) + internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTextCharacters) { var fontRenderingEmSize = shapedTextCharacters.Properties.FontRenderingEmSize; var fontMetrics = shapedTextCharacters.FontMetrics; @@ -181,16 +180,20 @@ namespace Avalonia.Media break; } + var origin = new Point(); + switch (Location) { - case TextDecorationLocation.Overline: - origin += new Point(0, fontMetrics.Ascent); + case TextDecorationLocation.Baseline: + origin += shapedTextCharacters.GlyphRun.BaselineOrigin; break; case TextDecorationLocation.Strikethrough: - origin += new Point(0, -fontMetrics.StrikethroughPosition); + origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X, + shapedTextCharacters.GlyphRun.BaselineOrigin.Y - fontMetrics.StrikethroughPosition); break; case TextDecorationLocation.Underline: - origin += new Point(0, -fontMetrics.UnderlinePosition); + origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X, + shapedTextCharacters.GlyphRun.BaselineOrigin.Y - fontMetrics.UnderlinePosition); break; } @@ -207,7 +210,7 @@ namespace Avalonia.Media var pen = new Pen(Stroke ?? shapedTextCharacters.Properties.ForegroundBrush, thickness, new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); - drawingContext.DrawLine(pen, origin, origin + new Point(shapedTextCharacters.Bounds.Width, 0)); + drawingContext.DrawLine(pen, origin, origin + new Point(shapedTextCharacters.Size.Width, 0)); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs index 56790cc0db..338c92f6b1 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs @@ -6,15 +6,14 @@ public abstract class DrawableTextRun : TextRun { /// - /// Gets the bounds. + /// Gets the size. /// - public abstract Rect Bounds { get; } + public abstract Size Size { get; } /// /// Draws the at the given origin. /// /// The drawing context. - /// The origin. - public abstract void Draw(DrawingContext drawingContext, Point origin); + public abstract void Draw(DrawingContext drawingContext); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index 9e67a03f45..09ecc0a026 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting public override int TextSourceLength { get; } /// - public override Rect Bounds => GlyphRun.Bounds; + public override Size Size => GlyphRun.Size; /// /// Gets the font metrics. @@ -45,7 +45,7 @@ namespace Avalonia.Media.TextFormatting public GlyphRun GlyphRun { get; } /// - public override void Draw(DrawingContext drawingContext, Point origin) + public override void Draw(DrawingContext drawingContext) { if (GlyphRun.GlyphIndices.Length == 0) { @@ -64,11 +64,10 @@ namespace Avalonia.Media.TextFormatting if (Properties.BackgroundBrush != null) { - drawingContext.DrawRectangle(Properties.BackgroundBrush, null, - new Rect(origin.X, origin.Y + FontMetrics.Ascent, Bounds.Width, Bounds.Height)); + drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size)); } - drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun, origin); + drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun); if (Properties.TextDecorations == null) { @@ -77,7 +76,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textDecoration in Properties.TextDecorations) { - textDecoration.Draw(drawingContext, this, origin); + textDecoration.Draw(drawingContext, this); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index 47e716982c..b91a50a27c 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -70,10 +70,11 @@ namespace Avalonia.Media.TextFormatting var codepoint = Codepoint.ReadAt(text, count, out _); //ToDo: Fix FontFamily fallback - currentTypeface = - FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.FontFamily); + var matchFound = + FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, + defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); - if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) + if (matchFound && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) { //Fallback found return new ShapeableTextCharacters(text.Take(count), diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index b116249fd4..3e85f0f6f0 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -52,7 +52,7 @@ namespace Avalonia.Media.TextFormatting { var glyphRun = textCharacters.GlyphRun; - if (glyphRun.Bounds.Width < availableWidth) + if (glyphRun.Size.Width < availableWidth) { return glyphRun.Characters.Length; } @@ -348,7 +348,7 @@ namespace Avalonia.Media.TextFormatting { var currentRun = textRuns[runIndex]; - if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) + if (currentWidth + currentRun.Size.Width > availableWidth) { var measuredLength = MeasureCharacters(currentRun, paragraphWidth - currentWidth); @@ -421,7 +421,7 @@ namespace Avalonia.Media.TextFormatting return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak); } - currentWidth += currentRun.GlyphRun.Bounds.Width; + currentWidth += currentRun.Size.Width; currentLength += currentRun.GlyphRun.Characters.Length; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index df1ecb4067..daa8807bf6 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -115,22 +115,24 @@ namespace Avalonia.Media.TextFormatting /// Draws the text layout. /// /// The drawing context. - /// The origin. - public void Draw(DrawingContext context, Point origin) + public void Draw(DrawingContext context) { if (!TextLines.Any()) { return; } - var currentY = origin.Y; + var currentY = 0.0; foreach (var textLine in TextLines) { var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width, _paragraphProperties.TextAlignment); - textLine.Draw(context, new Point(origin.X + offsetX, currentY)); + using (context.PushPostTransform(Matrix.CreateTranslation(offsetX, currentY))) + { + textLine.Draw(context); + } currentY += textLine.LineMetrics.Size.Height; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs index c052fb8948..8a1efa0611 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs @@ -51,8 +51,7 @@ namespace Avalonia.Media.TextFormatting /// Draws the at the given origin. /// /// The drawing context. - /// The origin. - public abstract void Draw(DrawingContext drawingContext, Point origin); + public abstract void Draw(DrawingContext drawingContext); /// /// Create a collapsed line based on collapsed text properties. diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index 51092cddda..f5e87d097b 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -33,17 +33,18 @@ namespace Avalonia.Media.TextFormatting public override bool HasCollapsed { get; } /// - public override void Draw(DrawingContext drawingContext, Point origin) + public override void Draw(DrawingContext drawingContext) { - var currentX = origin.X; + var currentX = 0.0; foreach (var textRun in _textRuns) { - var baselineOrigin = new Point(currentX, origin.Y + LineMetrics.TextBaseline); - - textRun.Draw(drawingContext, baselineOrigin); + using (drawingContext.PushPostTransform(Matrix.CreateTranslation(currentX, 0))) + { + textRun.Draw(drawingContext); + } - currentX += textRun.Bounds.Width; + currentX += textRun.Size.Width; } } @@ -64,13 +65,13 @@ namespace Avalonia.Media.TextFormatting var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol); - var availableWidth = collapsingProperties.Width - shapedSymbol.Bounds.Width; + var availableWidth = collapsingProperties.Width - shapedSymbol.Size.Width; while (runIndex < _textRuns.Count) { var currentRun = _textRuns[runIndex]; - currentWidth += currentRun.GlyphRun.Bounds.Width; + currentWidth += currentRun.Size.Width; if (currentWidth > availableWidth) { @@ -125,7 +126,7 @@ namespace Avalonia.Media.TextFormatting return new TextLineImpl(shapedTextCharacters, textLineMetrics, TextLineBreak, true); } - availableWidth -= currentRun.GlyphRun.Bounds.Width; + availableWidth -= currentRun.Size.Width; collapsedLength += currentRun.GlyphRun.Characters.Length; @@ -133,7 +134,7 @@ namespace Avalonia.Media.TextFormatting } textLineMetrics = - new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Bounds.Width), + new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Size.Width), LineMetrics.TextBaseline, TextRange, LineMetrics.HasOverflowed); return new TextLineImpl(new List(_textRuns) { shapedSymbol }, textLineMetrics, null, @@ -156,12 +157,12 @@ namespace Avalonia.Media.TextFormatting { characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _); - if (distance <= run.Bounds.Width) + if (distance <= run.Size.Width) { break; } - distance -= run.Bounds.Width; + distance -= run.Size.Width; } return characterHit; @@ -229,7 +230,7 @@ namespace Avalonia.Media.TextFormatting { if (codepointIndex > textRun.Text.End) { - currentDistance += textRun.Bounds.Width; + currentDistance += textRun.Size.Width; continue; } @@ -405,7 +406,7 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < shapedTextCharacters.Count; i++) { - shapedWidth += shapedTextCharacters[i].Bounds.Width; + shapedWidth += shapedTextCharacters[i].Size.Width; } return shapedWidth; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs index 6875cc1c04..c4d7527659 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs @@ -67,7 +67,7 @@ namespace Avalonia.Media.TextFormatting var fontMetrics = new FontMetrics(shapedRun.Properties.Typeface, shapedRun.Properties.FontRenderingEmSize); - lineWidth += shapedRun.Bounds.Width; + lineWidth += shapedRun.Size.Width; if (ascent > fontMetrics.Ascent) { diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 677e930804..17824c3c5e 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -8,17 +8,15 @@ namespace Avalonia.Media /// Represents a typeface. /// [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")] - public class Typeface : IEquatable + public readonly struct Typeface : IEquatable { - private GlyphTypeface _glyphTypeface; - /// /// Initializes a new instance of the class. /// /// The font family. /// The font style. /// The font weight. - public Typeface([NotNull]FontFamily fontFamily, + public Typeface([NotNull] FontFamily fontFamily, FontStyle style = FontStyle.Normal, FontWeight weight = FontWeight.Normal) { @@ -45,7 +43,7 @@ namespace Avalonia.Media { } - public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default); + public static Typeface Default { get; } = new Typeface(FontFamily.Default); /// /// Gets the font family. @@ -68,7 +66,7 @@ namespace Avalonia.Media /// /// The glyph typeface. /// - public GlyphTypeface GlyphTypeface => _glyphTypeface ?? (_glyphTypeface = new GlyphTypeface(this)); + public GlyphTypeface GlyphTypeface => FontManager.Current.GetOrAddGlyphTypeface(this); public static bool operator !=(Typeface a, Typeface b) { @@ -77,32 +75,17 @@ namespace Avalonia.Media public static bool operator ==(Typeface a, Typeface b) { - if (ReferenceEquals(a, b)) - { - return true; - } - - return !(a is null) && a.Equals(b); + return a.Equals(b); } public override bool Equals(object obj) { - if (obj is Typeface typeface) - { - return Equals(typeface); - } - - return false; + return obj is Typeface typeface && Equals(typeface); } public bool Equals(Typeface other) { - if (other is null) - { - return false; - } - - return FontFamily.Equals(other.FontFamily) && Style == other.Style && Weight == other.Weight; + return FontFamily == other.FontFamily && Style == other.Style && Weight == other.Weight; } public override int GetHashCode() diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index c87946b3ea..019614ae80 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -84,8 +84,7 @@ namespace Avalonia.Platform /// /// The foreground. /// The glyph run. - /// The baseline origin of the glyph run. - void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin); + void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun); /// /// Creates a new that can be used as a render layer diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 59b08aae0a..e562b45ca8 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; -using Avalonia.Media.Fonts; namespace Avalonia.Platform { @@ -26,13 +25,13 @@ namespace Avalonia.Platform /// The font weight. /// The font family. This is optional and used for fallback lookup. /// The culture. - /// The matching font key. + /// The matching typeface. /// /// True, if the could match the character to specified parameters, False otherwise. /// bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontFamily fontFamily, CultureInfo culture, out FontKey fontKey); + FontFamily fontFamily, CultureInfo culture, out Typeface typeface); /// /// Creates a glyph typeface. diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index 4cce2c925b..7324f5fbd0 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -267,5 +267,24 @@ namespace Avalonia { return new Point(_x, y); } + + /// + /// Deconstructs the point into its X and Y coordinates. + /// + /// The X coordinate. + /// The Y coordinate. + public void Deconstruct(out double x, out double y) + { + x = this._x; + y = this._y; + } + + /// + /// Gets a value indicating whether the X and Y coordinates are zero. + /// + public bool IsDefault + { + get { return (_x == 0) && (_y == 0); } + } } } diff --git a/src/Avalonia.Visuals/Rendering/RenderLayer.cs b/src/Avalonia.Visuals/Rendering/RenderLayer.cs index d6676e25ff..ddf5f4e5cf 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayer.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayer.cs @@ -30,12 +30,13 @@ namespace Avalonia.Rendering { if (Size != size || Scaling != scaling) { + Bitmap.Dispose(); var resized = RefCountable.Create(drawingContext.CreateLayer(size)); using (var context = resized.Item.CreateDrawingContext(null)) { context.Clear(Colors.Transparent); - Bitmap.Dispose(); + Bitmap = resized; Scaling = scaling; Size = size; diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index b37d5d660b..5c9cace4cd 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering _useManualFpsCounting = useManualFpsCounting; _fpsText = new FormattedText { - Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default), + Typeface = new Typeface(FontFamily.Default), FontSize = s_fontSize }; } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 4a364998fd..cb6b1f59d4 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -204,13 +204,13 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) { - Add(new GlyphRunNode(Transform, foreground, glyphRun, baselineOrigin, CreateChildScene(foreground))); + Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); } else diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs index bdf05c4f86..a6dba1bd32 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.VisualTree; @@ -17,20 +18,17 @@ namespace Avalonia.Rendering.SceneGraph /// The transform. /// The foreground brush. /// The glyph run to draw. - /// The baseline origin of the glyph run. /// Child scenes for drawing visual brushes. public GlyphRunNode( Matrix transform, IBrush foreground, GlyphRun glyphRun, - Point baselineOrigin, IDictionary childScenes = null) - : base(glyphRun.Bounds.Translate(baselineOrigin), transform) + : base(new Rect(glyphRun.Size), transform) { Transform = transform; Foreground = foreground?.ToImmutable(); GlyphRun = glyphRun; - BaselineOrigin = baselineOrigin; ChildScenes = childScenes; } @@ -49,11 +47,6 @@ namespace Avalonia.Rendering.SceneGraph /// public GlyphRun GlyphRun { get; } - /// - /// Gets the baseline origin. - /// - public Point BaselineOrigin { get; set; } - /// public override IDictionary ChildScenes { get; } @@ -61,7 +54,7 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { context.Transform = Transform; - context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + context.DrawGlyphRun(Foreground, GlyphRun); } /// diff --git a/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs index 79e029ccd2..9cc94ffac3 100644 --- a/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs +++ b/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs @@ -45,14 +45,13 @@ namespace Avalonia.Rendering void LoopProc() { - var now = _st.Elapsed; - var lastTick = now; - + var lastTick = _st.Elapsed; while (true) { + var now = _st.Elapsed; var timeTillNextTick = lastTick + _timeBetweenTicks - now; if (timeTillNextTick.TotalMilliseconds > 1) Thread.Sleep(timeTillNextTick); - + lastTick = now; lock (_lock) { if (_count == 0) @@ -63,7 +62,7 @@ namespace Avalonia.Rendering } _tick?.Invoke(now); - now = _st.Elapsed; + } } diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index f87b336b50..d87d2c5fc2 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.cs @@ -276,5 +276,24 @@ namespace Avalonia { return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _width, _height); } + + /// + /// Deconstructs the size into its Width and Height values. + /// + /// The width. + /// The height. + public void Deconstruct(out double width, out double height) + { + width = this._width; + height = this._height; + } + + /// + /// Gets a value indicating whether the Width and Height values are zero. + /// + public bool IsDefault + { + get { return (_width == 0) && (_height == 0); } + } } } diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index b03e91bf34..6d69c4d9a9 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.cs @@ -272,5 +272,28 @@ namespace Avalonia { return $"{_left},{_top},{_right},{_bottom}"; } + + /// + /// Deconstructor the thickness into its left, top, right and bottom thickness values. + /// + /// The thickness on the left. + /// The thickness on the top. + /// The thickness on the right. + /// The thickness on the bottom. + public void Deconstruct(out double left, out double top, out double right, out double bottom) + { + left = this._left; + top = this._top; + right = this._right; + bottom = this._bottom; + } + + /// + /// Gets a value indicating whether the left, top, right and bottom thickness values are zero. + /// + public bool IsDefault + { + get { return (_left == 0) && (_top == 0) && (_right == 0) && (_bottom == 0); } + } } } diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 6059dc3971..2fcf804f14 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -333,5 +333,24 @@ namespace Avalonia /// public static Vector UnitY => new Vector(0, 1); + + /// + /// Deconstructs the vector into its X and Y components. + /// + /// The X component. + /// The Y component. + public void Deconstruct(out double x, out double y) + { + x = this._x; + y = this._y; + } + + /// + /// Gets a value indicating whether the X and Y components are zero. + /// + public bool IsDefault + { + get { return (_x == 0) && (_y == 0); } + } } } diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index cd6eb6aac7..283d9deb52 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -455,7 +455,7 @@ namespace Avalonia } /// - /// Called when the control is added to a visual tree. + /// Called when the control is added to a rooted visual tree. /// /// The event args. protected virtual void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) @@ -463,7 +463,7 @@ namespace Avalonia } /// - /// Called when the control is removed from a visual tree. + /// Called when the control is removed from a rooted visual tree. /// /// The event args. protected virtual void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index 0349a6e26e..e9cb88cb8f 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -8,18 +8,21 @@ namespace Avalonia.X11.Glx { public IntPtr Handle { get; } public GlxInterface Glx { get; } + private readonly GlxContext _sharedWith; private readonly X11Info _x11; private readonly IntPtr _defaultXid; private readonly bool _ownsPBuffer; private readonly object _lock = new object(); - public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display, + public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display, + GlxContext sharedWith, GlVersion version, int sampleCount, int stencilSize, X11Info x11, IntPtr defaultXid, bool ownsPBuffer) { Handle = handle; Glx = glx; + _sharedWith = sharedWith; _x11 = x11; _defaultXid = defaultXid; _ownsPBuffer = ownsPBuffer; @@ -37,25 +40,21 @@ namespace Avalonia.X11.Glx public int SampleCount { get; } public int StencilSize { get; } - public IDisposable Lock() - { - Monitor.Enter(_lock); - return Disposable.Create(() => Monitor.Exit(_lock)); - } - class RestoreContext : IDisposable { private GlxInterface _glx; private IntPtr _defaultDisplay; + private readonly object _l; private IntPtr _display; private IntPtr _context; private IntPtr _read; private IntPtr _draw; - public RestoreContext(GlxInterface glx, IntPtr defaultDisplay) + public RestoreContext(GlxInterface glx, IntPtr defaultDisplay, object l) { _glx = glx; _defaultDisplay = defaultDisplay; + _l = l; _display = _glx.GetCurrentDisplay(); _context = _glx.GetCurrentContext(); _read = _glx.GetCurrentReadDrawable(); @@ -66,19 +65,49 @@ namespace Avalonia.X11.Glx { var disp = _display == IntPtr.Zero ? _defaultDisplay : _display; _glx.MakeContextCurrent(disp, _draw, _read, _context); + Monitor.Exit(_l); } } public IDisposable MakeCurrent() => MakeCurrent(_defaultXid); + public IDisposable EnsureCurrent() + { + if(IsCurrent) + return Disposable.Empty; + return MakeCurrent(); + } + + public bool IsSharedWith(IGlContext context) + { + var c = (GlxContext)context; + return c == this + || c._sharedWith == this + || _sharedWith == context + || _sharedWith != null && _sharedWith == c._sharedWith; + } public IDisposable MakeCurrent(IntPtr xid) { - var old = new RestoreContext(Glx, _x11.Display); - if (!Glx.MakeContextCurrent(_x11.Display, xid, xid, Handle)) - throw new OpenGlException("glXMakeContextCurrent failed "); - return old; + Monitor.Enter(_lock); + var success = false; + try + { + var old = new RestoreContext(Glx, _x11.Display, _lock); + if (!Glx.MakeContextCurrent(_x11.Display, xid, xid, Handle)) + throw new OpenGlException("glXMakeContextCurrent failed "); + + success = true; + return old; + } + finally + { + if (!success) + Monitor.Exit(_lock); + } } + public bool IsCurrent => Glx.GetCurrentContext() == Handle; + public void Dispose() { Glx.DestroyContext(_x11.Display, Handle); diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 903d6b570b..fa8c866c09 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -18,7 +18,7 @@ namespace Avalonia.X11.Glx public XVisualInfo* VisualInfo => _visual; public GlxContext DeferredContext { get; } public GlxInterface Glx { get; } = new GlxInterface(); - public GlxDisplay(X11Info x11, List probeProfiles) + public GlxDisplay(X11Info x11, IList probeProfiles) { _x11 = x11; _probeProfiles = probeProfiles.ToList(); @@ -113,9 +113,9 @@ namespace Avalonia.X11.Glx } - public GlxContext CreateContext() => CreateContext(DeferredContext); - - GlxContext CreateContext(IGlContext share) => CreateContext(CreatePBuffer(), share, + public GlxContext CreateContext() => CreateContext(); + + public GlxContext CreateContext(IGlContext share) => CreateContext(CreatePBuffer(), share, share.SampleCount, share.StencilSize, true); GlxContext CreateContext(IntPtr defaultXid, IGlContext share, @@ -144,7 +144,7 @@ namespace Avalonia.X11.Glx if (handle != IntPtr.Zero) { _version = profile; - return new GlxContext(new GlxInterface(), handle, this, profile, + return new GlxContext(new GlxInterface(), handle, this, (GlxContext)share, profile, sampleCount, stencilSize, _x11, defaultXid, ownsPBuffer); } diff --git a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs index ae6b0eb353..cb4ab4aca0 100644 --- a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs +++ b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs @@ -1,5 +1,8 @@ using System; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; +using static Avalonia.OpenGL.GlConsts; namespace Avalonia.X11.Glx { @@ -40,33 +43,26 @@ namespace Avalonia.X11.Glx public IGlPlatformSurfaceRenderingSession BeginDraw() { - var l = _context.Lock(); - try - { - - return new Session(_context, _info, l, _context.MakeCurrent(_info.Handle)); - } - catch - { - l.Dispose(); - throw; - } + var oldContext = _context.MakeCurrent(_info.Handle); + + // Reset to default FBO first + _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, 0); + + return new Session(_context, _info, oldContext); } class Session : IGlPlatformSurfaceRenderingSession { private readonly GlxContext _context; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - private IDisposable _lock; private readonly IDisposable _clearContext; public IGlContext Context => _context; public Session(GlxContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info, - IDisposable @lock, IDisposable clearContext) + IDisposable clearContext) { _context = context; _info = info; - _lock = @lock; _clearContext = clearContext; } @@ -77,7 +73,6 @@ namespace Avalonia.X11.Glx _context.Display.SwapBuffers(_info.Handle); _context.Glx.WaitX(); _clearContext.Dispose(); - _lock.Dispose(); } public PixelSize Size => _info.Size; diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index e3250e6733..6735a32ffe 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -5,31 +5,34 @@ using Avalonia.OpenGL; namespace Avalonia.X11.Glx { - class GlxGlPlatformFeature : IWindowingPlatformGlFeature + class GlxPlatformOpenGlInterface : IPlatformOpenGlInterface { public GlxDisplay Display { get; private set; } + public bool CanCreateContexts => true; + public bool CanShareContexts => true; public IGlContext CreateContext() => Display.CreateContext(); + public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryContext); public GlxContext DeferredContext { get; private set; } - public IGlContext MainContext => DeferredContext; + public IGlContext PrimaryContext => DeferredContext; - public static bool TryInitialize(X11Info x11, List glProfiles) + public static bool TryInitialize(X11Info x11, IList glProfiles) { var feature = TryCreate(x11, glProfiles); if (feature != null) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); return true; } return false; } - public static GlxGlPlatformFeature TryCreate(X11Info x11, List glProfiles) + public static GlxPlatformOpenGlInterface TryCreate(X11Info x11, IList glProfiles) { try { var disp = new GlxDisplay(x11, glProfiles); - return new GlxGlPlatformFeature + return new GlxPlatformOpenGlInterface { Display = disp, DeferredContext = disp.DeferredContext diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 7f3255d4da..c6db146f7b 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -7,6 +7,7 @@ using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.X11; @@ -70,9 +71,9 @@ namespace Avalonia.X11 if (options.UseGpu) { if (options.UseEGL) - EglGlPlatformFeature.TryInitialize(); + EglPlatformOpenGlInterface.TryInitialize(); else - GlxGlPlatformFeature.TryInitialize(Info, Options.GlProfiles); + GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); } @@ -103,7 +104,7 @@ namespace Avalonia public bool UseDBusMenu { get; set; } public bool UseDeferredRendering { get; set; } = true; - public List GlProfiles { get; set; } = new List + public IList GlProfiles { get; set; } = new List { new GlVersion(GlProfileType.OpenGL, 4, 0), new GlVersion(GlProfileType.OpenGL, 3, 2), @@ -113,7 +114,7 @@ namespace Avalonia new GlVersion(GlProfileType.OpenGLES, 2, 0) }; - public List GlxRendererBlacklist { get; set; } = new List + public IList GlxRendererBlacklist { get; set; } = new List { // llvmpipe is a software GL rasterizer. If it's returned by glGetString, // that usually means that something in the system is horribly misconfigured diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 0c0b942bcd..2cd3b973d8 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -12,6 +12,7 @@ using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -65,7 +66,7 @@ namespace Avalonia.X11 _touch = new TouchDevice(); _keyboard = platform.KeyboardDevice; - var glfeature = AvaloniaLocator.Current.GetService(); + var glfeature = AvaloniaLocator.Current.GetService(); XSetWindowAttributes attr = new XSetWindowAttributes(); var valueMask = default(SetWindowValuemask); @@ -87,13 +88,13 @@ namespace Avalonia.X11 // OpenGL seems to be do weird things to it's current window which breaks resize sometimes _useRenderWindow = glfeature != null; - var glx = glfeature as GlxGlPlatformFeature; + var glx = glfeature as GlxPlatformOpenGlInterface; if (glx != null) visualInfo = *glx.Display.VisualInfo; else if (glfeature == null) visualInfo = _x11.TransparentVisualInfo; - var egl = glfeature as EglGlPlatformFeature; + var egl = glfeature as EglPlatformOpenGlInterface; var visual = IntPtr.Zero; var depth = 24; @@ -168,7 +169,7 @@ namespace Avalonia.X11 if (egl != null) surfaces.Insert(0, - new EglGlPlatformSurface(egl.DeferredContext, + new EglGlPlatformSurface(egl, new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); if (glx != null) surfaces.Insert(0, new GlxGlPlatformSurface(glx.Display, glx.DeferredContext, diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index db37e4af0b..8801f71f9a 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -32,8 +32,8 @@ namespace Avalonia.LinuxFramebuffer void Initialize() { Threading = new InternalPlatformThreadingInterface(); - if (_fb is IWindowingPlatformGlFeature glFeature) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(glFeature); + if (_fb is IGlOutputBackend gl) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) .Bind().ToConstant(new DefaultRenderTimer(60)) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 7a5d20fc83..72eed9e543 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -4,6 +4,8 @@ using System.ComponentModel; using System.Linq; using System.Runtime.InteropServices; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; using Avalonia.Platform.Interop; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; using static Avalonia.LinuxFramebuffer.Output.LibDrm; @@ -11,13 +13,16 @@ using static Avalonia.LinuxFramebuffer.Output.LibDrm.GbmColorFormats; namespace Avalonia.LinuxFramebuffer.Output { - public unsafe class DrmOutput : IOutputBackend, IGlPlatformSurface, IWindowingPlatformGlFeature + public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface { private DrmCard _card; private readonly EglGlPlatformSurface _eglPlatformSurface; public PixelSize PixelSize => _mode.Resolution; public double Scaling { get; set; } - public IGlContext MainContext => _deferredContext; + public IGlContext PrimaryContext => _deferredContext; + + private EglPlatformOpenGlInterface _platformGl; + public IPlatformOpenGlInterface PlatformOpenGlInterface => _platformGl; public DrmOutput(string path = null) { @@ -132,10 +137,9 @@ namespace Avalonia.LinuxFramebuffer.Output if(_gbmTargetSurface == null) throw new InvalidOperationException("Unable to create GBM surface"); - - - _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), 0x31D7, device, null); - _eglSurface = _eglDisplay.CreateWindowSurface(_gbmTargetSurface); + _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), false, 0x31D7, device, null); + _platformGl = new EglPlatformOpenGlInterface(_eglDisplay); + _eglSurface = _platformGl.CreateWindowSurface(_gbmTargetSurface); EglContext CreateContext(EglContext share) @@ -144,7 +148,7 @@ namespace Avalonia.LinuxFramebuffer.Output GbmBoFlags.GBM_BO_USE_RENDERING); if (offSurf == null) throw new InvalidOperationException("Unable to create 1x1 sized GBM surface"); - return _eglDisplay.CreateContext(share, _eglDisplay.CreateWindowSurface(offSurf)); + return _eglDisplay.CreateContext(share, _platformGl.CreateWindowSurface(offSurf)); } _deferredContext = CreateContext(null); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs new file mode 100644 index 0000000000..7bc73d590c --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs @@ -0,0 +1,9 @@ +using Avalonia.OpenGL; + +namespace Avalonia.LinuxFramebuffer.Output +{ + public interface IGlOutputBackend : IOutputBackend + { + public IPlatformOpenGlInterface PlatformOpenGlInterface { get; } + } +} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index a155fd863b..98528a128a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -401,16 +401,16 @@ namespace Avalonia.Skia } /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { - using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; ConfigureTextRendering(paintWrapper); - Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X, - (float)baselineOrigin.Y, paintWrapper.Paint); + Canvas.DrawText(glyphRunImpl.TextBlob, (float)glyphRun.BaselineOrigin.X, + (float)glyphRun.BaselineOrigin.Y, paintWrapper.Paint); } } diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 91bc937475..62ec39346a 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Platform; using SkiaSharp; @@ -31,7 +30,7 @@ namespace Avalonia.Skia public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + FontFamily fontFamily, CultureInfo culture, out Typeface fontKey) { SKFontStyle skFontStyle; @@ -81,7 +80,7 @@ namespace Avalonia.Skia continue; } - fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); + fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight); return true; } @@ -92,7 +91,7 @@ namespace Avalonia.Skia if (skTypeface != null) { - fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); + fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight); return true; } diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index 987e2c089c..1a7a9b75cf 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -19,6 +19,6 @@ namespace Avalonia.Skia public interface IOpenGlAwareSkiaGpu : ISkiaGpu { - IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap(); + IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 081db5d26a..6df8df9a4c 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using Avalonia.OpenGL; +using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; using SkiaSharp; diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index 9278de2137..46d42dfdab 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; +using Avalonia.OpenGL.Surfaces; using SkiaSharp; namespace Avalonia.Skia @@ -8,10 +9,12 @@ namespace Avalonia.Skia class GlSkiaGpu : IOpenGlAwareSkiaGpu { private GRContext _grContext; + private IGlContext _glContext; - public GlSkiaGpu(IWindowingPlatformGlFeature gl, long? maxResourceBytes) + public GlSkiaGpu(IPlatformOpenGlInterface openGl, long? maxResourceBytes) { - var context = gl.MainContext; + var context = openGl.PrimaryContext; + _glContext = context; using (context.MakeCurrent()) { using (var iface = context.Version.Type == GlProfileType.OpenGL ? @@ -40,6 +43,6 @@ namespace Avalonia.Skia return null; } - public IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap() => new OpenGlTextureBitmapImpl(); + public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) => new GlOpenGlBitmapImpl(_glContext, size, dpi); } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs new file mode 100644 index 0000000000..2ebf7c680b --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Imaging; +using Avalonia.Utilities; +using SkiaSharp; +using static Avalonia.OpenGL.GlConsts; + +namespace Avalonia.Skia +{ + class GlOpenGlBitmapImpl : IOpenGlBitmapImpl, IDrawableBitmapImpl + { + private readonly IGlContext _context; + private readonly object _lock = new object(); + private IGlPresentableOpenGlSurface _surface; + + public GlOpenGlBitmapImpl(IGlContext context, PixelSize pixelSize, Vector dpi) + { + _context = context; + PixelSize = pixelSize; + Dpi = dpi; + } + + public Vector Dpi { get; } + public PixelSize PixelSize { get; } + public int Version { get; private set; } + public void Save(string fileName) => throw new NotSupportedException(); + + public void Save(Stream stream) => throw new NotSupportedException(); + + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + lock (_lock) + { + if (_surface == null) + return; + using (_surface.Lock()) + { + using (var backendTexture = new GRBackendTexture(PixelSize.Width, PixelSize.Height, false, + new GRGlTextureInfo( + GlConsts.GL_TEXTURE_2D, (uint)_surface.GetTextureId(), + (uint)_surface.InternalFormat))) + using (var surface = SKSurface.Create(context.GrContext, backendTexture, GRSurfaceOrigin.TopLeft, + SKColorType.Rgba8888)) + { + // Again, silently ignore, if something went wrong it's not our fault + if (surface == null) + return; + + using (var snapshot = surface.Snapshot()) + context.Canvas.DrawImage(snapshot, sourceRect, destRect, paint); + } + + } + } + } + + public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback) + { + if (!SupportsContext(context)) + throw new OpenGlException("Context is not supported for texture sharing"); + return new SharedOpenGlBitmapAttachment(this, context, presentCallback); + } + + public bool SupportsContext(IGlContext context) + { + // TODO: negotiated platform surface sharing + return _context.IsSharedWith(context); + } + + public void Dispose() + { + + } + + internal void Present(IGlPresentableOpenGlSurface surface) + { + lock (_lock) + { + _surface = surface; + } + } + } + + interface IGlPresentableOpenGlSurface : IDisposable + { + int GetTextureId(); + int InternalFormat { get; } + IDisposable Lock(); + } + + class SharedOpenGlBitmapAttachment : IOpenGlBitmapAttachment, IGlPresentableOpenGlSurface + { + private readonly GlOpenGlBitmapImpl _bitmap; + private readonly IGlContext _context; + private readonly Action _presentCallback; + private readonly int _fbo; + private readonly int _texture; + private readonly int _frontBuffer; + private bool _disposed; + private readonly DisposableLock _lock = new DisposableLock(); + + public SharedOpenGlBitmapAttachment(GlOpenGlBitmapImpl bitmap, IGlContext context, Action presentCallback) + { + _bitmap = bitmap; + _context = context; + _presentCallback = presentCallback; + using (_context.EnsureCurrent()) + { + var glVersion = _context.Version; + InternalFormat = glVersion.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8; + + _context.GlInterface.GetIntegerv(GL_FRAMEBUFFER_BINDING, out _fbo); + if (_fbo == 0) + throw new OpenGlException("Current FBO is 0"); + + { + var gl = _context.GlInterface; + + var textures = new int[2]; + gl.GenTextures(2, textures); + _texture = textures[0]; + _frontBuffer = textures[1]; + + gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); + foreach (var t in textures) + { + gl.BindTexture(GL_TEXTURE_2D, t); + gl.TexImage2D(GL_TEXTURE_2D, 0, + InternalFormat, + _bitmap.PixelSize.Width, _bitmap.PixelSize.Height, + 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); + + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + } + + gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); + gl.BindTexture(GL_TEXTURE_2D, oldTexture); + + } + } + } + + public void Present() + { + using (_context.MakeCurrent()) + { + if (_disposed) + throw new ObjectDisposedException(nameof(SharedOpenGlBitmapAttachment)); + + var gl = _context.GlInterface; + + gl.Finish(); + using (Lock()) + { + gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var oldFbo); + gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); + gl.GetIntegerv(GL_ACTIVE_TEXTURE, out var oldActive); + + gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo); + gl.BindTexture(GL_TEXTURE_2D, _frontBuffer); + gl.ActiveTexture(GL_TEXTURE0); + + gl.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, _bitmap.PixelSize.Width, + _bitmap.PixelSize.Height); + + gl.BindFramebuffer(GL_FRAMEBUFFER, oldFbo); + gl.BindTexture(GL_TEXTURE_2D, oldTexture); + gl.ActiveTexture(oldActive); + + gl.Finish(); + } + } + + _bitmap.Present(this); + _presentCallback(); + } + + public void Dispose() + { + var gl = _context.GlInterface; + _bitmap.Present(null); + + if(_disposed) + return; + using (_context.MakeCurrent()) + using (Lock()) + { + if(_disposed) + return; + _disposed = true; + gl.DeleteTextures(2, new[] { _texture, _frontBuffer }); + } + } + + int IGlPresentableOpenGlSurface.GetTextureId() + { + return _frontBuffer; + } + + public int InternalFormat { get; } + + public IDisposable Lock() => _lock.Lock(); + } +} diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs deleted file mode 100644 index 8d007e35f3..0000000000 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.IO; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; -using Avalonia.Skia.Helpers; -using Avalonia.Utilities; -using SkiaSharp; - -namespace Avalonia.Skia -{ - class OpenGlTextureBitmapImpl : IOpenGlTextureBitmapImpl, IDrawableBitmapImpl - { - private DisposableLock _lock = new DisposableLock(); - private int _textureId; - private int _internalFormat; - - public void Dispose() - { - using (Lock()) - { - _textureId = 0; - PixelSize = new PixelSize(1, 1); - Version++; - } - } - - public Vector Dpi { get; private set; } = new Vector(96, 96); - public PixelSize PixelSize { get; private set; } = new PixelSize(1, 1); - public int Version { get; private set; } = 0; - - public void Save(string fileName) => throw new System.NotSupportedException(); - public void Save(Stream stream) => throw new System.NotSupportedException(); - - public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) - { - // For now silently ignore - if (context.GrContext == null) - return; - - using (Lock()) - { - if (_textureId == 0) - return; - using (var backendTexture = new GRBackendTexture(PixelSize.Width, PixelSize.Height, false, - new GRGlTextureInfo( - GlConsts.GL_TEXTURE_2D, (uint)_textureId, - (uint)_internalFormat))) - using (var surface = SKSurface.Create(context.GrContext, backendTexture, GRSurfaceOrigin.TopLeft, - SKColorType.Rgba8888)) - { - // Again, silently ignore, if something went wrong it's not our fault - if (surface == null) - return; - - using (var snapshot = surface.Snapshot()) - context.Canvas.DrawImage(snapshot, sourceRect, destRect, paint); - } - } - } - - public IDisposable Lock() => _lock.Lock(); - - public void SetBackBuffer(int textureId, int internalFormat, PixelSize pixelSize, double dpiScaling) - { - using (_lock.Lock()) - { - _textureId = textureId; - _internalFormat = internalFormat; - PixelSize = pixelSize; - Dpi = new Vector(96 * dpiScaling, 96 * dpiScaling); - Version++; - } - } - - public void SetDirty() - { - using (_lock.Lock()) - Version++; - } - } -} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index c4f70df7c0..b9c1cbc673 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -30,7 +30,7 @@ namespace Avalonia.Skia return; } - var gl = AvaloniaLocator.Current.GetService(); + var gl = AvaloniaLocator.Current.GetService(); if (gl != null) _skiaGpu = new GlSkiaGpu(gl, maxResourceBytes); } @@ -256,10 +256,10 @@ namespace Avalonia.Skia } - public IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap() + public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) { if (_skiaGpu is IOpenGlAwareSkiaGpu glAware) - return glAware.CreateOpenGlTextureBitmap(); + return glAware.CreateOpenGlBitmap(size, dpi); if (_skiaGpu == null) throw new PlatformNotSupportedException("GPU acceleration is not available"); throw new PlatformNotSupportedException( diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 6c2ac17923..71deb1235f 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -2,31 +2,28 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Avalonia.Media; -using Avalonia.Media.Fonts; using SkiaSharp; namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary _typefaces = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary _typefaces = + new ConcurrentDictionary(); - public void AddTypeface(FontKey key, SKTypeface typeface) + public void AddTypeface(Typeface key, SKTypeface typeface) { _typefaces.TryAdd(key, typeface); } public SKTypeface Get(Typeface typeface) { - var key = new FontKey(typeface.FontFamily.Name, typeface.Style, typeface.Weight); - - return GetNearestMatch(_typefaces, key); + return GetNearestMatch(_typefaces, typeface); } - private static SKTypeface GetNearestMatch(IDictionary typefaces, FontKey key) + private static SKTypeface GetNearestMatch(IDictionary typefaces, Typeface key) { - if (typefaces.TryGetValue(new FontKey(key.FamilyName, key.Style, key.Weight), out var typeface)) + if (typefaces.TryGetValue(key, out var typeface)) { return typeface; } @@ -42,7 +39,7 @@ namespace Avalonia.Skia { if (weight - j >= 100) { - if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) + if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) { return typeface; } @@ -53,7 +50,7 @@ namespace Avalonia.Skia continue; } - if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) + if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) { return typeface; } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 7ca44e7282..de77c9186e 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -56,7 +56,7 @@ namespace Avalonia.Skia continue; } - var key = new FontKey(fontFamily.Name, typeface.FontSlant.ToAvalonia(), + var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(), (FontWeight)typeface.FontWeight); typeFaceCollection.AddTypeface(key, typeface); diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index cbe0b5ef42..493263677d 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -16,6 +16,10 @@ namespace Avalonia /// /// The maximum number of bytes for video memory to store textures and resources. /// - public long? MaxGpuResourceSizeBytes { get; set; } + /// + /// This is set by default to the recommended value for Avalonia. + /// Setting this to null will give you the default Skia value. + /// + public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures. } } diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 27b29c6e1e..428087ac56 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -109,10 +109,16 @@ namespace Avalonia.Skia /// public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) { - using (var image = SnapshotImage()) + if (sourceRect.Left == 0 && sourceRect.Top == 0 && sourceRect.Size == destRect.Size) { - context.Canvas.DrawImage(image, sourceRect, destRect, paint); + _surface.Canvas.Flush(); + _surface.Draw(context.Canvas, destRect.Left, destRect.Top, paint); } + else + using (var image = SnapshotImage()) + { + context.Canvas.DrawImage(image, sourceRect, destRect, paint); + } } /// diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index e0de40525f..258a51db5a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -324,13 +324,14 @@ namespace Avalonia.Direct2D1.Media /// The foreground. /// The glyph run. /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { - using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; - _renderTarget.DrawGlyphRun(baselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural); + _renderTarget.DrawGlyphRun(glyphRun.BaselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, + brush.PlatformBrush, MeasuringMode.Natural); } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 33af15076d..6d95d759ec 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Platform; using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; @@ -34,7 +33,7 @@ namespace Avalonia.Direct2D1.Media public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) + FontFamily fontFamily, CultureInfo culture, out Typeface typeface) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -51,12 +50,12 @@ namespace Avalonia.Direct2D1.Media var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - fontKey = new FontKey(fontFamilyName, fontStyle, fontWeight); + typeface = new Typeface(fontFamilyName, fontStyle, fontWeight); return true; } - fontKey = default; + typeface = default; return false; } diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 585e68056b..fbc56e7703 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,25 +1,27 @@ using Avalonia.OpenGL; +using Avalonia.OpenGL.Angle; +using Avalonia.OpenGL.Egl; namespace Avalonia.Win32 { static class Win32GlManager { /// This property is initialized if drawing platform requests OpenGL support - public static EglGlPlatformFeature EglFeature { get; private set; } + public static EglPlatformOpenGlInterface EglPlatformInterface { get; private set; } private static bool s_attemptedToInitialize; public static void Initialize() { - AvaloniaLocator.CurrentMutable.Bind().ToFunc(() => + AvaloniaLocator.CurrentMutable.Bind().ToFunc(() => { if (!s_attemptedToInitialize) { - EglFeature = EglGlPlatformFeature.TryCreate(); + EglPlatformInterface = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); s_attemptedToInitialize = true; } - return EglFeature; + return EglPlatformInterface; }); } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ddc0cc4e42..cb85e14e5a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -7,6 +7,8 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Win32.Input; @@ -103,8 +105,8 @@ namespace Avalonia.Win32 CreateWindow(); _framebuffer = new FramebufferManager(_hwnd); - if (Win32GlManager.EglFeature != null) - _gl = new EglGlPlatformSurface(Win32GlManager.EglFeature.DeferredContext, this); + if (Win32GlManager.EglPlatformInterface != null) + _gl = new EglGlPlatformSurface(Win32GlManager.EglPlatformInterface, this); Screen = new ScreenImpl(); @@ -853,7 +855,7 @@ namespace Avalonia.Win32 private void ShowWindow(WindowState state) { - ShowWindowCommand command; + ShowWindowCommand? command; var newWindowProperties = _windowProperties; @@ -875,8 +877,8 @@ namespace Avalonia.Win32 case WindowState.FullScreen: newWindowProperties.IsFullScreen = true; - UpdateWindowProperties(newWindowProperties); - return; + command = IsWindowVisible(_hwnd) ? (ShowWindowCommand?)null : ShowWindowCommand.Restore; + break; default: throw new ArgumentException("Invalid WindowState."); @@ -884,7 +886,10 @@ namespace Avalonia.Win32 UpdateWindowProperties(newWindowProperties); - UnmanagedMethods.ShowWindow(_hwnd, command); + if (command.HasValue) + { + UnmanagedMethods.ShowWindow(_hwnd, command.Value); + } if (state == WindowState.Maximized) { @@ -1007,10 +1012,12 @@ namespace Avalonia.Win32 if (newProperties.IsResizable) { style |= WindowStyles.WS_SIZEFRAME; + style |= WindowStyles.WS_MAXIMIZEBOX; } else { style &= ~WindowStyles.WS_SIZEFRAME; + style &= ~WindowStyles.WS_MAXIMIZEBOX; } SetStyle(style); diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index 635df43407..f9c787b6a8 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -1,15 +1,18 @@ using System; +using System.Reactive.Disposables; using Avalonia.OpenGL; using OpenGLES; using OpenTK.Graphics.ES30; namespace Avalonia.iOS { - class EaglFeature : IWindowingPlatformGlFeature + class EaglFeature : IPlatformOpenGlInterface { + public IGlContext PrimaryContext => Context; + public IGlContext CreateSharedContext() => throw new NotSupportedException(); + public bool CanShareContexts => false; + public bool CanCreateContexts => false; public IGlContext CreateContext() => throw new System.NotSupportedException(); - - public IGlContext MainContext => Context; public GlContext Context { get; } = new GlContext(); } @@ -61,9 +64,18 @@ namespace Avalonia.iOS return new ResetContext(old); } + public IDisposable EnsureCurrent() + { + if(EAGLContext.CurrentContext == Context) + return Disposable.Empty; + return MakeCurrent(); + } + + public bool IsSharedWith(IGlContext context) => false; + public GlVersion Version { get; } = new GlVersion(GlProfileType.OpenGLES, 3, 0); public GlInterface GlInterface { get; } public int SampleCount { get; } = 0; public int StencilSize { get; } = 9; } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs index 64912b8ae3..5e5e1da949 100644 --- a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs +++ b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs @@ -2,6 +2,7 @@ using System; using System.Threading; using Avalonia.OpenGL; +using Avalonia.OpenGL.Surfaces; using CoreAnimation; using OpenTK.Graphics.ES30; @@ -91,4 +92,4 @@ namespace Avalonia.iOS } } } -} \ No newline at end of file +} diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index b484559ff3..28bccb6637 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -26,7 +26,7 @@ namespace Avalonia.iOS var keyboard = new KeyboardDevice(); var softKeyboard = new SoftKeyboardHelper(); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(GlFeature) + .Bind().ToConstant(GlFeature) .Bind().ToConstant(new CursorFactoryStub()) .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToConstant(new ClipboardImpl()) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 81a8de1046..20172eea88 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -94,8 +94,8 @@ namespace Avalonia.Base.UnitTests Class1.FooProperty.Changed.Subscribe(e => raised = e.Property == Class1.FooProperty && - (string)e.OldValue == "initial" && - (string)e.NewValue == "newvalue" && + e.OldValue.GetValueOrDefault() == "initial" && + e.NewValue.GetValueOrDefault() == "newvalue" && e.Priority == BindingPriority.LocalValue); target.SetValue(Class1.FooProperty, "newvalue"); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index d7f927372e..8e5d8b7be2 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -83,7 +83,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); string value = null; - Class1.FooProperty.Changed.Subscribe(x => value = (string)x.NewValue); + Class1.FooProperty.Changed.Subscribe(x => value = x.NewValue.GetValueOrDefault()); target.SetValue(Class1.FooProperty, "newvalue"); Assert.Equal("newvalue", value); @@ -95,7 +95,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); var result = new List(); - Class1.FooProperty.Changed.Subscribe(x => result.Add((string)x.NewValue)); + Class1.FooProperty.Changed.Subscribe(x => result.Add(x.NewValue.GetValueOrDefault())); target.SetValue(Class1.FooProperty, "animated", BindingPriority.Animation); target.SetValue(Class1.FooProperty, "local"); diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs index 19700cadab..d5ac01a092 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs +++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; @@ -334,5 +335,27 @@ namespace Avalonia.Base.UnitTests.Collections Assert.True(raised); } + + [Fact] + public void Can_CopyTo_Array_Of_Same_Type() + { + var target = new AvaloniaList { "foo", "bar", "baz" }; + var result = new string[3]; + + target.CopyTo(result, 0); + + Assert.Equal(target, result); + } + + [Fact] + public void Can_CopyTo_Array_Of_Base_Type() + { + var target = new AvaloniaList { "foo", "bar", "baz" }; + var result = new object[3]; + + ((IList)target).CopyTo(result, 0); + + Assert.Equal(target, result); + } } } diff --git a/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs b/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs index a1a734f650..04d7ce3fc7 100644 --- a/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs +++ b/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Linq; using Avalonia.Collections; using Xunit; @@ -18,7 +19,7 @@ namespace Avalonia.Controls.DataGrid.UnitTests.Collections new Item("c", "c"), }; var expectedResult = items.OrderBy(i => i.Prop1).ToList(); - var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), @descending: false); + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), ListSortDirection.Ascending); sortDescription.Initialize(typeof(Item)); var result = sortDescription.OrderBy(items).ToList(); @@ -36,7 +37,7 @@ namespace Avalonia.Controls.DataGrid.UnitTests.Collections new Item("c", "c"), }; var expectedResult = items.OrderByDescending(i => i.Prop1).ToList(); - var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), @descending: true); + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), ListSortDirection.Descending); sortDescription.Initialize(typeof(Item)); var result = sortDescription.OrderBy(items).ToList(); @@ -61,7 +62,7 @@ namespace Avalonia.Controls.DataGrid.UnitTests.Collections new Item("a", "b"), new Item("a", "c"), }; - var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), @descending: false); + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), ListSortDirection.Ascending); sortDescription.Initialize(typeof(Item)); var result = sortDescription.ThenBy(items).ToList(); @@ -86,7 +87,7 @@ namespace Avalonia.Controls.DataGrid.UnitTests.Collections new Item("a", "b"), new Item("a", "a"), }; - var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), @descending: true); + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), ListSortDirection.Descending); sortDescription.Initialize(typeof(Item)); var result = sortDescription.ThenBy(items).ToList(); diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 57cea91834..3e78e951e2 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -363,6 +363,40 @@ namespace Avalonia.Controls.UnitTests }); } + [Fact] + public void Custom_TextSelector() + { + RunTest((control, textbox) => + { + object selectedItem = control.Items.Cast().First(); + string input = "42"; + + control.TextSelector = (text, item) => text + item; + Assert.Equal(control.TextSelector("4", "2"), "42"); + + control.Text = input; + control.SelectedItem = selectedItem; + Assert.Equal(control.Text, control.TextSelector(input, selectedItem.ToString())); + }); + } + + [Fact] + public void Custom_ItemSelector() + { + RunTest((control, textbox) => + { + object selectedItem = control.Items.Cast().First(); + string input = "42"; + + control.ItemSelector = (text, item) => text + item; + Assert.Equal(control.ItemSelector("4", 2), "42"); + + control.Text = input; + control.SelectedItem = selectedItem; + Assert.Equal(control.Text, control.ItemSelector(input, selectedItem)); + }); + } + /// /// Retrieves a defined predicate filter through a new AutoCompleteBox /// control instance. diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 5bad646aa7..f032186bcd 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -438,6 +438,53 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Focusable_Controls_In_Popup_Should_Get_Focus() + { + using (CreateServicesWithFocus()) + { + var window = PreparedWindow(); + + var tb = new TextBox(); + var b = new Button(); + var p = new Popup + { + PlacementTarget = window, + Child = new StackPanel + { + Children = + { + tb, + b + } + } + }; + ((ISetLogicalParent)p).SetParent(p.PlacementTarget); + window.Show(); + + p.Open(); + + if(p.Host is OverlayPopupHost host) + { + //Need to measure/arrange for visual children to show up + //in OverlayPopupHost + host.Measure(Size.Infinity); + host.Arrange(new Rect(host.DesiredSize)); + } + + tb.Focus(); + + Assert.True(FocusManager.Instance?.Current == tb); + + //Ensure focus remains in the popup + var nextFocus = KeyboardNavigationHandler.GetNext(FocusManager.Instance.Current, NavigationDirection.Next); + + Assert.True(nextFocus == b); + + p.Close(); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: @@ -450,6 +497,21 @@ namespace Avalonia.Controls.UnitTests.Primitives }))); } + private IDisposable CreateServicesWithFocus() + { + return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: + new MockWindowingPlatform(null, + x => + { + if (UsePopupHost) + return null; + return MockWindowingPlatform.CreatePopupMock(x).Object; + }), + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice())); + } + + private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source, Point p) { var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 00d148093a..514d3b5475 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -554,6 +554,44 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.False(items.Single().IsSelected); } + [Fact] + public void Removing_Selected_Item_Should_Update_Selection_With_AlwaysSelected() + { + var item0 = new Item(); + var item1 = new Item(); + var items = new AvaloniaList + { + item0, + item1, + }; + + var target = new TestSelector + { + Items = items, + Template = Template(), + SelectionMode = SelectionMode.AlwaysSelected, + }; + + Prepare(target); + target.SelectedIndex = 1; + + Assert.Equal(items[1], target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + + SelectionChangedEventArgs receivedArgs = null; + + target.SelectionChanged += (_, args) => receivedArgs = args; + + items.RemoveAt(1); + + Assert.Same(item0, target.SelectedItem); + Assert.Equal(0, target.SelectedIndex); + Assert.NotNull(receivedArgs); + Assert.Equal(new[] { item0 }, receivedArgs.AddedItems); + Assert.Equal(new[] { item1 }, receivedArgs.RemovedItems); + Assert.True(items.Single().IsSelected); + } + [Fact] public void Removing_Selected_Item_Should_Clear_Selection_With_BeginInit() { @@ -771,6 +809,186 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(called); } + [Fact] + public void Setting_SelectedIndex_Should_Raise_PropertyChanged_Events() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + + var target = new TestSelector + { + Items = items, + Template = Template(), + }; + + var selectedIndexRaised = 0; + var selectedItemRaised = 0; + + target.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + Assert.Equal(-1, e.OldValue); + Assert.Equal(1, e.NewValue); + ++selectedIndexRaised; + } + else if (e.Property == SelectingItemsControl.SelectedItemProperty) + { + Assert.Null(e.OldValue); + Assert.Equal("bar", e.NewValue); + ++selectedItemRaised; + } + }; + + target.SelectedIndex = 1; + + Assert.Equal(1, selectedIndexRaised); + Assert.Equal(1, selectedItemRaised); + } + + [Fact] + public void Removing_Selected_Item_Should_Raise_PropertyChanged_Events() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + + var target = new TestSelector + { + Items = items, + Template = Template(), + }; + + var selectedIndexRaised = 0; + var selectedItemRaised = 0; + target.SelectedIndex = 1; + + target.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + Assert.Equal(1, e.OldValue); + Assert.Equal(-1, e.NewValue); + ++selectedIndexRaised; + } + else if (e.Property == SelectingItemsControl.SelectedItemProperty) + { + Assert.Equal("bar", e.OldValue); + Assert.Null(e.NewValue); + } + }; + + items.RemoveAt(1); + + Assert.Equal(1, selectedIndexRaised); + Assert.Equal(0, selectedItemRaised); + } + + [Fact] + public void Removing_Selected_Item0_Should_Raise_PropertyChanged_Events_With_AlwaysSelected() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + + var target = new TestSelector + { + Items = items, + Template = Template(), + SelectionMode = SelectionMode.AlwaysSelected, + }; + + var selectedIndexRaised = 0; + var selectedItemRaised = 0; + target.SelectedIndex = 0; + + target.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + ++selectedIndexRaised; + } + else if (e.Property == SelectingItemsControl.SelectedItemProperty) + { + Assert.Equal("foo", e.OldValue); + Assert.Equal("bar", e.NewValue); + ++selectedItemRaised; + } + }; + + items.RemoveAt(0); + + Assert.Equal(0, selectedIndexRaised); + Assert.Equal(1, selectedItemRaised); + } + + [Fact] + public void Removing_Selected_Item1_Should_Raise_PropertyChanged_Events_With_AlwaysSelected() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + + var target = new TestSelector + { + Items = items, + Template = Template(), + SelectionMode = SelectionMode.AlwaysSelected, + }; + + var selectedIndexRaised = 0; + var selectedItemRaised = 0; + target.SelectedIndex = 1; + + target.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + Assert.Equal(1, e.OldValue); + Assert.Equal(0, e.NewValue); + ++selectedIndexRaised; + } + else if (e.Property == SelectingItemsControl.SelectedItemProperty) + { + Assert.Equal("bar", e.OldValue); + Assert.Equal("foo", e.NewValue); + } + }; + + items.RemoveAt(1); + + Assert.Equal(1, selectedIndexRaised); + Assert.Equal(0, selectedItemRaised); + } + + [Fact] + public void Removing_Item_Before_Selection_Should_Raise_PropertyChanged_Events() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + + var target = new SelectingItemsControl + { + Items = items, + Template = Template(), + }; + + var selectedIndexRaised = 0; + var selectedItemRaised = 0; + target.SelectedIndex = 1; + + target.PropertyChanged += (s, e) => + { + if (e.Property == SelectingItemsControl.SelectedIndexProperty) + { + Assert.Equal(1, e.OldValue); + Assert.Equal(0, e.NewValue); + ++selectedIndexRaised; + } + else if (e.Property == SelectingItemsControl.SelectedItemProperty) + { + ++selectedItemRaised; + } + }; + + items.RemoveAt(0); + + Assert.Equal(1, selectedIndexRaised); + Assert.Equal(0, selectedItemRaised); + } + [Fact] public void Order_Of_Setting_Items_And_SelectedIndex_During_Initialization_Should_Not_Matter() { @@ -1184,12 +1402,36 @@ namespace Avalonia.Controls.UnitTests.Primitives Items = items, }; + var raised = false; + Prepare(target); + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); + target.SelectedIndex = 2; + + Assert.True(raised); + } + + [Fact] + public void AutoScrollToSelectedItem_Causes_Scroll_To_Initial_SelectedItem() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + }; var raised = false; - target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); target.SelectedIndex = 2; + Prepare(target); Assert.True(raised); } @@ -1233,6 +1475,99 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void AutoScrollToSelectedItem_Scrolls_When_Reattached_To_Visual_Tree_If_Selection_Changed_While_Detached_From_Visual_Tree() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectedIndex = 2, + }; + + var raised = false; + + Prepare(target); + + var root = (TestRoot)target.Parent; + + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); + + root.Child = null; + target.SelectedIndex = 1; + root.Child = target; + + Assert.True(raised); + } + + [Fact] + public void AutoScrollToSelectedItem_Doesnt_Scroll_If_Reattached_To_Visual_Tree_With_No_Selection_Change() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectedIndex = 2, + }; + + var raised = false; + + Prepare(target); + + var root = (TestRoot)target.Parent; + + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); + + root.Child = null; + root.Child = target; + + Assert.False(raised); + } + + [Fact] + public void AutoScrollToSelectedItem_Causes_Scroll_When_Turned_On() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + AutoScrollToSelectedItem = false, + }; + + Prepare(target); + + var raised = false; + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); + target.SelectedIndex = 2; + + Assert.False(raised); + + target.AutoScrollToSelectedItem = true; + + Assert.True(raised); + } + [Fact] public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization() { @@ -1376,6 +1711,190 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "foo" }, target.SelectedItems); } + [Fact] + public void Preserves_Initial_SelectedItems_When_Bound() + { + // Issue #4272 (there are two issues there, this addresses the second one). + var vm = new SelectionViewModel + { + Items = { "foo", "bar", "baz" }, + SelectedItems = { "bar" }, + }; + + var target = new ListBox + { + [!ListBox.ItemsProperty] = new Binding("Items"), + [!ListBox.SelectedItemsProperty] = new Binding("SelectedItems"), + DataContext = vm, + }; + + Prepare(target); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { 1 }, target.Selection.SelectedIndexes); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + } + + [Fact] + public void Preserves_SelectedItem_When_Items_Changed() + { + // Issue #4048 + var target = new SelectingItemsControl + { + Items = new[] { "foo", "bar", "baz"}, + SelectedItem = "bar", + }; + + Prepare(target); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("bar", target.SelectedItem); + + target.Items = new[] { "qux", "foo", "bar" }; + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal("bar", target.SelectedItem); + } + + [Fact] + public void Setting_SelectedItems_Raises_PropertyChanged() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz" }, + }; + + var raised = 0; + var newValue = new AvaloniaList(); + + Prepare(target); + + target.PropertyChanged += (s, e) => + { + if (e.Property == ListBox.SelectedItemsProperty) + { + Assert.Null(e.OldValue); + Assert.Same(newValue, e.NewValue); + ++raised; + } + }; + + target.SelectedItems = newValue; + + Assert.Equal(1, raised); + } + + [Fact] + public void Setting_Selection_Raises_SelectedItems_PropertyChanged() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz" }, + }; + + var raised = 0; + var oldValue = target.SelectedItems; + + Prepare(target); + + target.PropertyChanged += (s, e) => + { + if (e.Property == ListBox.SelectedItemsProperty) + { + Assert.Same(oldValue, e.OldValue); + Assert.Null(e.NewValue); + ++raised; + } + }; + + target.Selection = new SelectionModel(); + + Assert.Equal(1, raised); + } + + [Fact] + public void Handles_Removing_Last_Item_In_Two_Controls_With_Bound_SelectedIndex() + { + var items = new ObservableCollection { "foo" }; + + // Simulates problem with TabStrip and Carousel with bound SelectedIndex. + var tabStrip = new TestSelector + { + Items = items, + SelectionMode = SelectionMode.AlwaysSelected, + }; + + var carousel = new TestSelector + { + Items = items, + [!Carousel.SelectedIndexProperty] = tabStrip[!TabStrip.SelectedIndexProperty], + }; + + var tabStripRaised = 0; + var carouselRaised = 0; + + tabStrip.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { "foo" }, e.RemovedItems); + Assert.Empty(e.AddedItems); + ++tabStripRaised; + }; + + carousel.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { "foo" }, e.RemovedItems); + Assert.Empty(e.AddedItems); + ++carouselRaised; + }; + + items.RemoveAt(0); + + Assert.Equal(1, tabStripRaised); + Assert.Equal(1, carouselRaised); + } + + [Fact] + public void Handles_Removing_Last_Item_In_Controls_With_Bound_SelectedItem() + { + var items = new ObservableCollection { "foo" }; + + // Simulates problem with TabStrip and Carousel with bound SelectedItem. + var tabStrip = new TestSelector + { + Items = items, + SelectionMode = SelectionMode.AlwaysSelected, + }; + + var carousel = new TestSelector + { + Items = items, + [!Carousel.SelectedItemProperty] = tabStrip[!TabStrip.SelectedItemProperty], + }; + + var tabStripRaised = 0; + var carouselRaised = 0; + + tabStrip.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { "foo" }, e.RemovedItems); + Assert.Empty(e.AddedItems); + ++tabStripRaised; + }; + + carousel.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { "foo" }, e.RemovedItems); + Assert.Empty(e.AddedItems); + ++carouselRaised; + }; + + items.RemoveAt(0); + + Assert.Equal(1, tabStripRaised); + Assert.Equal(1, carouselRaised); + } + private static void Prepare(SelectingItemsControl target) { var root = new TestRoot @@ -1445,6 +1964,7 @@ namespace Avalonia.Controls.UnitTests.Primitives public SelectionViewModel() { Items = new ObservableCollection(); + SelectedItems = new ObservableCollection(); } public int SelectedIndex @@ -1458,6 +1978,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } public ObservableCollection Items { get; } + public ObservableCollection SelectedItems { get; } } private class RootWithItems : TestRoot @@ -1484,6 +2005,12 @@ namespace Avalonia.Controls.UnitTests.Primitives set => base.Selection = value; } + public new IList SelectedItems + { + get => base.SelectedItems; + set => base.SelectedItems = value; + } + public new SelectionMode SelectionMode { get => base.SelectionMode; diff --git a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs new file mode 100644 index 0000000000..b64812e290 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Collections; +using Avalonia.Controls.Selection; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Selection +{ + public class InternalSelectionModelTests + { + [Fact] + public void Selecting_Item_Adds_To_WritableSelectedItems() + { + var target = CreateTarget(); + + target.Select(0); + + Assert.Equal(new[] { "foo" }, target.WritableSelectedItems); + } + + [Fact] + public void Selecting_Duplicate_On_Model_Adds_To_WritableSelectedItems() + { + var target = CreateTarget(source: new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); + + target.SelectRange(1, 4); + + Assert.Equal(new[] { "bar", "baz", "foo", "bar" }, target.WritableSelectedItems); + } + + [Fact] + public void Deselecting_On_Model_Removes_SelectedItem() + { + var target = CreateTarget(); + + target.SelectRange(1, 2); + target.Deselect(1); + + Assert.Equal(new[] { "baz" }, target.WritableSelectedItems); + } + + [Fact] + public void Deselecting_Duplicate_On_Model_Removes_SelectedItem() + { + var target = CreateTarget(source: new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); + + target.SelectRange(1, 2); + target.Select(4); + target.Deselect(4); + + Assert.Equal(new[] { "baz", "bar" }, target.WritableSelectedItems); + } + + [Fact] + public void Adding_To_WritableSelectedItems_Selects_On_Model() + { + var target = CreateTarget(); + + target.SelectRange(1, 2); + target.WritableSelectedItems.Add("foo"); + + Assert.Equal(new[] { 0, 1, 2 }, target.SelectedIndexes); + Assert.Equal(new[] { "bar", "baz", "foo" }, target.WritableSelectedItems); + } + + [Fact] + public void Removing_From_WritableSelectedItems_Deselects_On_Model() + { + var target = CreateTarget(); + + target.SelectRange(1, 2); + target.WritableSelectedItems.Remove("baz"); + + Assert.Equal(new[] { 1 }, target.SelectedIndexes); + Assert.Equal(new[] { "bar" }, target.WritableSelectedItems); + } + + [Fact] + public void Replacing_SelectedItem_Updates_Model() + { + var target = CreateTarget(); + + target.SelectRange(1, 2); + target.WritableSelectedItems[0] = "foo"; + + Assert.Equal(new[] { 0, 2 }, target.SelectedIndexes); + Assert.Equal(new[] { "foo", "baz" }, target.WritableSelectedItems); + } + + [Fact] + public void Clearing_WritableSelectedItems_Updates_Model() + { + var target = CreateTarget(); + + target.WritableSelectedItems.Clear(); + + Assert.Empty(target.SelectedIndexes); + } + + [Fact] + public void Setting_WritableSelectedItems_Updates_Model() + { + var target = CreateTarget(); + var oldItems = target.WritableSelectedItems; + + var newItems = new AvaloniaList { "foo", "baz" }; + target.WritableSelectedItems = newItems; + + Assert.Equal(new[] { 0, 2 }, target.SelectedIndexes); + Assert.Same(newItems, target.WritableSelectedItems); + Assert.NotSame(oldItems, target.WritableSelectedItems); + Assert.Equal(new[] { "foo", "baz" }, newItems); + } + + [Fact] + public void Setting_Items_To_Null_Clears_Selection() + { + var target = CreateTarget(); + + target.SelectRange(1, 2); + target.WritableSelectedItems = null; + + Assert.Empty(target.SelectedIndexes); + } + + [Fact] + public void Setting_Items_To_Null_Creates_Empty_Items() + { + var target = CreateTarget(); + var oldItems = target.WritableSelectedItems; + + target.WritableSelectedItems = null; + + Assert.NotNull(target.WritableSelectedItems); + Assert.NotSame(oldItems, target.WritableSelectedItems); + Assert.IsType>(target.WritableSelectedItems); + } + + [Fact] + public void Adds_Null_WritableSelectedItems_When_Source_Is_Null() + { + var target = CreateTarget(nullSource: true); + + target.SelectRange(1, 2); + Assert.Equal(new object[] { null, null }, target.WritableSelectedItems); + } + + [Fact] + public void Updates_WritableSelectedItems_When_Source_Changes_From_Null() + { + var target = CreateTarget(nullSource: true); + + target.SelectRange(1, 2); + Assert.Equal(new object[] { null, null }, target.WritableSelectedItems); + + target.Source = new[] { "foo", "bar", "baz" }; + Assert.Equal(new[] { "bar", "baz" }, target.WritableSelectedItems); + } + + [Fact] + public void Updates_WritableSelectedItems_When_Source_Changes_To_Null() + { + var target = CreateTarget(); + + target.SelectRange(1, 2); + Assert.Equal(new[] { "bar", "baz" }, target.WritableSelectedItems); + + target.Source = null; + Assert.Equal(new object[] { null, null }, target.WritableSelectedItems); + } + + [Fact] + public void WritableSelectedItems_Can_Be_Set_Before_Source() + { + var target = CreateTarget(nullSource: true); + var items = new AvaloniaList { "foo", "bar", "baz" }; + var WritableSelectedItems = new AvaloniaList { "bar" }; + + target.WritableSelectedItems = WritableSelectedItems; + target.Source = items; + + Assert.Equal(1, target.SelectedIndex); + } + + [Fact] + public void Does_Not_Accept_Fixed_Size_Items() + { + var target = CreateTarget(); + + Assert.Throws(() => + target.WritableSelectedItems = new[] { "foo", "bar", "baz" }); + } + + [Fact] + public void Restores_Selection_On_Items_Reset() + { + var items = new ResettingCollection(new[] { "foo", "bar", "baz" }); + var target = CreateTarget(source: items); + + target.SelectedIndex = 1; + items.Reset(new[] { "baz", "foo", "bar" }); + + Assert.Equal(2, target.SelectedIndex); + } + + [Fact] + public void Preserves_Selection_On_Source_Changed() + { + var target = CreateTarget(); + + target.SelectedIndex = 1; + target.Source = new[] { "baz", "foo", "bar" }; + + Assert.Equal(2, target.SelectedIndex); + } + + private static InternalSelectionModel CreateTarget( + bool singleSelect = false, + IList source = null, + bool nullSource = false) + { + source ??= !nullSource ? new[] { "foo", "bar", "baz" } : null; + + var result = new InternalSelectionModel + { + SingleSelect = singleSelect, + }; + + ((ISelectionModel)result).Source = source; + return result; + } + + private class ResettingCollection : List, INotifyCollectionChanged + { + public ResettingCollection(IEnumerable items) + { + AddRange(items); + } + + public void Reset(IEnumerable items) + { + Clear(); + AddRange(items); + CollectionChanged?.Invoke( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs index 3eddd35465..5d0c6d31e1 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs @@ -121,6 +121,34 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(0, raised); } + [Fact] + public void Initializing_Source_Raises_SelectedItems_PropertyChanged() + { + var target = CreateTarget(false); + var selectedItemRaised = 0; + var selectedItemsRaised = 0; + + target.Select(1); + target.Select(2); + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.SelectedItem)) + { + ++selectedItemRaised; + } + else if (e.PropertyName == nameof(target.SelectedItems)) + { + ++selectedItemsRaised; + } + }; + + target.Source = new[] { "foo", "bar", "baz" }; + + Assert.Equal(1, selectedItemRaised); + Assert.Equal(1, selectedItemsRaised); + } + [Fact] public void Initializing_Source_Respects_Range_SourceItem_Order() { @@ -152,6 +180,34 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal("bar", target.SelectedItem); Assert.Equal(new[] { "bar" }, target.SelectedItems); } + + [Fact] + public void Changing_Source_To_Null_Raises_SelectedItems_PropertyChanged() + { + var target = CreateTarget(); + var selectedItemRaised = 0; + var selectedItemsRaised = 0; + + target.Select(1); + target.Select(2); + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.SelectedItem)) + { + ++selectedItemRaised; + } + else if (e.PropertyName == nameof(target.SelectedItems)) + { + ++selectedItemsRaised; + } + }; + + target.Source = null; + + Assert.Equal(1, selectedItemRaised); + Assert.Equal(1, selectedItemsRaised); + } } public class SelectedIndex diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index 1b37730797..66a2cef921 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs @@ -174,6 +174,33 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(new[] { "bar" }, target.SelectedItems); } + [Fact] + public void Initializing_Source_Raises_SelectedItems_PropertyChanged() + { + var target = CreateTarget(false); + var selectedItemRaised = 0; + var selectedItemsRaised = 0; + + target.Select(1); + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.SelectedItem)) + { + ++selectedItemRaised; + } + else if (e.PropertyName == nameof(target.SelectedItems)) + { + ++selectedItemsRaised; + } + }; + + target.Source = new[] { "foo", "bar", "baz" }; + + Assert.Equal(1, selectedItemRaised); + Assert.Equal(1, selectedItemsRaised); + } + [Fact] public void Changing_Source_To_Null_Doesnt_Clear_Selection() { @@ -194,7 +221,7 @@ namespace Avalonia.Controls.UnitTests.Selection } [Fact] - public void Changing_Source_To_NonNUll_First_Clears_Old_Selection() + public void Changing_Source_To_NonNull_First_Clears_Old_Selection() { var target = CreateTarget(); var raised = 0; @@ -219,6 +246,33 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(1, raised); } + [Fact] + public void Changing_Source_To_Null_Raises_SelectedItems_PropertyChanged() + { + var target = CreateTarget(); + var selectedItemRaised = 0; + var selectedItemsRaised = 0; + + target.Select(1); + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.SelectedItem)) + { + ++selectedItemRaised; + } + else if (e.PropertyName == nameof(target.SelectedItems)) + { + ++selectedItemsRaised; + } + }; + + target.Source = null; + + Assert.Equal(1, selectedItemRaised); + Assert.Equal(1, selectedItemsRaised); + } + [Fact] public void Raises_PropertyChanged() { diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index fd52aeb9af..e6f7ac601f 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -122,11 +122,86 @@ namespace Avalonia.Controls.UnitTests Items = collection, }; - target.ApplyTemplate(); + Prepare(target); target.SelectedItem = collection[1]; + + Assert.Same(collection[1], target.SelectedItem); + Assert.Equal(collection[1].Content, target.SelectedContent); + collection.RemoveAt(1); Assert.Same(collection[0], target.SelectedItem); + Assert.Equal(collection[0].Content, target.SelectedContent); + } + + [Fact] + public void Removal_Should_Set_New_Item0_When_Item0_Selected() + { + var collection = new ObservableCollection() + { + new TabItem + { + Name = "first", + Content = "foo", + }, + new TabItem + { + Name = "second", + Content = "bar", + }, + new TabItem + { + Name = "3rd", + Content = "barf", + }, + }; + + var target = new TabControl + { + Template = TabControlTemplate(), + Items = collection, + }; + + Prepare(target); + target.SelectedItem = collection[0]; + + Assert.Same(collection[0], target.SelectedItem); + Assert.Equal(collection[0].Content, target.SelectedContent); + + collection.RemoveAt(0); + + Assert.Same(collection[0], target.SelectedItem); + Assert.Equal(collection[0].Content, target.SelectedContent); + } + + [Fact] + public void Removal_Should_Set_New_Item0_When_Item0_Selected_With_DataTemplate() + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var collection = new ObservableCollection() + { + new Item("first"), + new Item("second"), + new Item("3rd"), + }; + + var target = new TabControl + { + Template = TabControlTemplate(), + Items = collection, + }; + + Prepare(target); + target.SelectedItem = collection[0]; + + Assert.Same(collection[0], target.SelectedItem); + Assert.Equal(collection[0], target.SelectedContent); + + collection.RemoveAt(0); + + Assert.Same(collection[0], target.SelectedItem); + Assert.Equal(collection[0], target.SelectedContent); } [Fact] @@ -383,6 +458,13 @@ namespace Avalonia.Controls.UnitTests }.RegisterInNameScope(scope)); } + private void Prepare(TabControl target) + { + ApplyTemplate(target); + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + } + private void ApplyTemplate(TabControl target) { target.ApplyTemplate(); diff --git a/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs deleted file mode 100644 index 3899d9dfbf..0000000000 --- a/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using Avalonia.Collections; -using Avalonia.Controls.Selection; -using Avalonia.Controls.Utils; -using Xunit; - -namespace Avalonia.Controls.UnitTests.Utils -{ - public class SelectedItemsSyncTests - { - [Fact] - public void Initial_Items_Are_From_Model() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - Assert.Equal(new[] { "bar", "baz" }, items); - } - - [Fact] - public void Selecting_On_Model_Adds_Item() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - target.SelectionModel.Select(0); - - Assert.Equal(new[] { "bar", "baz", "foo" }, items); - } - - [Fact] - public void Selecting_Duplicate_On_Model_Adds_Item() - { - var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); - var items = target.SelectedItems; - - target.SelectionModel.Select(4); - - Assert.Equal(new[] { "bar", "baz", "bar" }, items); - } - - [Fact] - public void Deselecting_On_Model_Removes_Item() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - target.SelectionModel.Deselect(1); - - Assert.Equal(new[] { "baz" }, items); - } - - [Fact] - public void Deselecting_Duplicate_On_Model_Removes_Item() - { - var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); - var items = target.SelectedItems; - - target.SelectionModel.Select(4); - target.SelectionModel.Deselect(4); - - Assert.Equal(new[] { "baz", "bar" }, items); - } - - [Fact] - public void Reassigning_Model_Resets_Items() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - var newModel = new SelectionModel - { - Source = (string[])target.SelectionModel.Source, - SingleSelect = false - }; - - newModel.Select(0); - newModel.Select(1); - - target.SelectionModel = newModel; - - Assert.Equal(new[] { "foo", "bar" }, items); - } - - [Fact] - public void Reassigning_Model_Tracks_New_Model() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - var newModel = new SelectionModel - { - Source = (string[])target.SelectionModel.Source, - SingleSelect = false - }; - - target.SelectionModel = newModel; - - newModel.Select(0); - newModel.Select(1); - - Assert.Equal(new[] { "foo", "bar" }, items); - } - - [Fact] - public void Adding_To_Items_Selects_On_Model() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - items.Add("foo"); - - Assert.Equal(new[] { 0, 1, 2 }, target.SelectionModel.SelectedIndexes); - Assert.Equal(new[] { "bar", "baz", "foo" }, items); - } - - [Fact] - public void Removing_From_Items_Deselects_On_Model() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - items.Remove("baz"); - - Assert.Equal(new[] { 1 }, target.SelectionModel.SelectedIndexes); - Assert.Equal(new[] { "bar" }, items); - } - - [Fact] - public void Replacing_Item_Updates_Model() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - items[0] = "foo"; - - Assert.Equal(new[] { 0, 2 }, target.SelectionModel.SelectedIndexes); - Assert.Equal(new[] { "foo", "baz" }, items); - } - - [Fact] - public void Clearing_Items_Updates_Model() - { - var target = CreateTarget(); - var items = target.SelectedItems; - - items.Clear(); - - Assert.Empty(target.SelectionModel.SelectedIndexes); - } - - [Fact] - public void Setting_Items_Updates_Model() - { - var target = CreateTarget(); - var oldItems = target.SelectedItems; - - var newItems = new AvaloniaList { "foo", "baz" }; - target.SelectedItems = newItems; - - Assert.Equal(new[] { 0, 2 }, target.SelectionModel.SelectedIndexes); - Assert.Same(newItems, target.SelectedItems); - Assert.NotSame(oldItems, target.SelectedItems); - Assert.Equal(new[] { "foo", "baz" }, newItems); - } - - [Fact] - public void Setting_Items_Subscribes_To_Model() - { - var target = CreateTarget(); - var items = new AvaloniaList { "foo", "baz" }; - - target.SelectedItems = items; - target.SelectionModel.Select(1); - - Assert.Equal(new[] { "foo", "baz", "bar" }, items); - } - - [Fact] - public void Setting_Items_To_Null_Creates_Empty_Items() - { - var target = CreateTarget(); - var oldItems = target.SelectedItems; - - target.SelectedItems = null; - - var newItems = Assert.IsType>(target.SelectedItems); - - Assert.NotSame(oldItems, newItems); - } - - [Fact] - public void Handles_Null_Model_Source() - { - var model = new SelectionModel { SingleSelect = false }; - model.Select(1); - - var target = new SelectedItemsSync(model); - var items = target.SelectedItems; - - Assert.Empty(items); - - model.Select(2); - model.Source = new[] { "foo", "bar", "baz" }; - - Assert.Equal(new[] { "bar", "baz" }, items); - } - - [Fact] - public void Does_Not_Accept_Fixed_Size_Items() - { - var target = CreateTarget(); - - Assert.Throws(() => - target.SelectedItems = new[] { "foo", "bar", "baz" }); - } - - [Fact] - public void Selected_Items_Can_Be_Set_Before_SelectionModel_Source() - { - var model = new SelectionModel(); - var target = new SelectedItemsSync(model); - var items = new AvaloniaList { "foo", "bar", "baz" }; - var selectedItems = new AvaloniaList { "bar" }; - - target.SelectedItems = selectedItems; - model.Source = items; - - Assert.Equal(1, model.SelectedIndex); - } - - [Fact] - public void Restores_Selection_On_Items_Reset() - { - var items = new ResettingCollection(new[] { "foo", "bar", "baz" }); - var model = new SelectionModel { Source = items }; - var target = new SelectedItemsSync(model); - - model.SelectedIndex = 1; - items.Reset(new[] { "baz", "foo", "bar" }); - - Assert.Equal(2, model.SelectedIndex); - } - - private static SelectedItemsSync CreateTarget( - IEnumerable items = null) - { - items ??= new[] { "foo", "bar", "baz" }; - - var model = new SelectionModel { Source = items, SingleSelect = false }; - model.SelectRange(1, 2); - - var target = new SelectedItemsSync(model); - return target; - } - - private class ResettingCollection : List, INotifyCollectionChanged - { - public ResettingCollection(IEnumerable items) - { - AddRange(items); - } - - public void Reset(IEnumerable items) - { - Clear(); - AddRange(items); - CollectionChanged?.Invoke( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - public event NotifyCollectionChangedEventHandler CollectionChanged; - } - } -} diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 0b81276240..0c7b966f29 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -552,6 +552,37 @@ namespace Avalonia.LeakTests } } + [Fact] + public void ItemsRepeater_Is_Freed() + { + using (Start()) + { + Func run = () => + { + var window = new Window + { + Content = new ItemsRepeater(), + }; + + window.Show(); + + window.LayoutManager.ExecuteInitialLayoutPass(); + Assert.IsType(window.Presenter.Child); + + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + } + private IDisposable Start() { return UnitTestApplication.Start(TestServices.StyledWindow.With( diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index 8683da9a01..7528424521 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -51,7 +51,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var r = AvaloniaLocator.Current.GetService(); return r.CreateFormattedText(text, - FontManager.Current.GetOrAddTypeface(fontFamily, fontStyle, fontWeight), + new Typeface(fontFamily, fontStyle, fontWeight), fontSize, textAlignment, wrapping, diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index f36d6d9e4a..a0fe348166 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -39,7 +39,7 @@ namespace Avalonia.Skia.UnitTests.Media private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, - CultureInfo culture, out FontKey fontKey) + CultureInfo culture, out Typeface typeface) { foreach (var customTypeface in _customTypefaces) { @@ -48,7 +48,7 @@ namespace Avalonia.Skia.UnitTests.Media continue; } - fontKey = new FontKey(customTypeface.FontFamily.Name, fontStyle, fontWeight); + typeface = new Typeface(customTypeface.FontFamily.Name, fontStyle, fontWeight); return true; } @@ -56,7 +56,7 @@ namespace Avalonia.Skia.UnitTests.Media var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint); - fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); + typeface = new Typeface(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); return true; } @@ -73,13 +73,13 @@ namespace Avalonia.Skia.UnitTests.Media skTypeface = typefaceCollection.Get(typeface); break; } - case "Noto Sans": { var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily); skTypeface = typefaceCollection.Get(typeface); break; } + case FontFamily.DefaultFontFamilyName: case "Noto Mono": { var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 4a88b259bc..adcc79e029 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -130,7 +130,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - const string text = "1234الدولي"; + const string text = "ABCDالدولي"; var defaultProperties = new GenericTextRunProperties(Typeface.Default); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index bf41381b52..f3e1c37705 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -369,7 +369,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var glyphRun = shapedRun.GlyphRun; - var width = glyphRun.Bounds.Width; + var width = glyphRun.Size.Width; var characterHit = glyphRun.GetCharacterHitFromDistance(width, out _); diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs index e614c60310..ba3b346f1b 100644 --- a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Platform; namespace Avalonia.UnitTests @@ -26,9 +25,9 @@ namespace Avalonia.UnitTests } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, - CultureInfo culture, out FontKey fontKey) + CultureInfo culture, out Typeface fontKey) { - fontKey = default; + fontKey = new Typeface(_defaultFamilyName); return false; } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs index 81a4ca6495..2b0ffa4ed6 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Media; -using Avalonia.Platform; using Avalonia.UnitTests; using Xunit; @@ -15,9 +14,11 @@ namespace Avalonia.Visuals.UnitTests.Media { var fontFamily = new FontFamily("MyFont"); - var typeface = FontManager.Current.GetOrAddTypeface(fontFamily); + var typeface = new Typeface(fontFamily); - Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily)); + var glyphTypeface = FontManager.Current.GetOrAddGlyphTypeface(typeface); + + Assert.Same(glyphTypeface, FontManager.Current.GetOrAddGlyphTypeface(typeface)); } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs index 219c7ece46..58feb4714a 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs @@ -119,6 +119,56 @@ namespace Avalonia.Visuals.UnitTests.Media } } + [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0)] + [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 1)] + [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 0)] + [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 1)] + [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 0)] + [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 1)] + [Theory] + public void Should_Find_Glyph_Index(double[] advances, ushort[] clusters, int bidiLevel) + { + using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel)) + { + if (glyphRun.IsLeftToRight) + { + for (var i = 0; i < clusters.Length; i++) + { + var cluster = clusters[i]; + + var found = glyphRun.FindGlyphIndex(cluster); + + var expected = i; + + while (expected - 1 >= 0 && clusters[expected - 1] == cluster) + { + expected--; + } + + Assert.Equal(expected, found); + } + } + else + { + for (var i = clusters.Length - 1; i > 0; i--) + { + var cluster = clusters[i]; + + var found = glyphRun.FindGlyphIndex(cluster); + + var expected = i; + + while (expected + 1 < clusters.Length && clusters[expected + 1] == cluster) + { + expected++; + } + + Assert.Equal(expected, found); + } + } + } + } + private static GlyphRun CreateGlyphRun(double[] glyphAdvances, ushort[] glyphClusters, int bidiLevel = 0) { var count = glyphAdvances.Length;