diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 76620e8b93..73e38f8cb9 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -23,8 +23,6 @@ "src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj", "src\\Avalonia.Fonts.Inter\\Avalonia.Fonts.Inter.csproj", "src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj", - "src\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", - "src\\Avalonia.Headless\\Avalonia.Headless.csproj", "src\\Avalonia.MicroCom\\Avalonia.MicroCom.csproj", "src\\Avalonia.Native\\Avalonia.Native.csproj", "src\\Avalonia.OpenGL\\Avalonia.OpenGL.csproj", @@ -33,6 +31,8 @@ "src\\Avalonia.Themes.Fluent\\Avalonia.Themes.Fluent.csproj", "src\\Avalonia.Themes.Simple\\Avalonia.Themes.Simple.csproj", "src\\Avalonia.X11\\Avalonia.X11.csproj", + "src\\Headless\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", + "src\\Headless\\Avalonia.Headless\\Avalonia.Headless.csproj", "src\\Linux\\Avalonia.LinuxFramebuffer\\Avalonia.LinuxFramebuffer.csproj", "src\\Markup\\Avalonia.Markup.Xaml.Loader\\Avalonia.Markup.Xaml.Loader.csproj", "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj", @@ -65,4 +65,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} \ No newline at end of file +} diff --git a/Avalonia.sln b/Avalonia.sln index f33b782479..d4ccdfdc69 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -181,9 +181,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Headless\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "src\Headless\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" 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 @@ -260,6 +260,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Desktop", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.iOS", "samples\SafeAreaDemo.iOS\SafeAreaDemo.iOS.csproj", "{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF237916-7150-496B-89ED-6CA3292896E7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.UnitTests", "tests\Avalonia.Headless.UnitTests\Avalonia.Headless.UnitTests.csproj", "{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -599,6 +605,14 @@ Global {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.Build.0 = Release|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -690,6 +704,10 @@ Global {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098} diff --git a/Directory.Build.targets b/Directory.Build.targets index 73954c7f4d..e8d4baba11 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,5 +1,5 @@ - + $(DefineConstants);NET7SDK diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index fdc144e3a5..6d1ff7cf12 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -12,7 +12,6 @@ { ComPtr _parent; NSTrackingArea* _area; - NSMutableAttributedString* _markedText; bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; AvnInputModifiers _modifierState; NSEvent* _lastMouseDownEvent; @@ -22,8 +21,9 @@ AvnPlatformResizeReason _resizeReason; AvnAccessibilityElement* _accessibilityChild; NSRect _cursorRect; - NSMutableString* _text; - NSRange _selection; + NSMutableAttributedString* _text; + NSRange _selectedRange; + NSRange _markedRange; } - (void)onClosed @@ -59,6 +59,11 @@ [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; _modifierState = AvnInputModifiersNone; + + _text = [[NSMutableAttributedString alloc] initWithString:@""]; + _markedRange = NSMakeRange(0, 0); + _selectedRange = NSMakeRange(0, 0); + return self; } @@ -521,9 +526,13 @@ - (void)keyDown:(NSEvent *)event { - [self keyboardEvent:event withType:KeyDown]; - _lastKeyHandled = [[self inputContext] handleEvent:event]; - [super keyDown:event]; + _lastKeyHandled = false; + + [[self inputContext] handleEvent:event]; + + if(!_lastKeyHandled){ + [self keyboardEvent:event withType:KeyDown]; + } } - (void)keyUp:(NSEvent *)event @@ -532,6 +541,10 @@ [super keyUp:event]; } +- (void) doCommandBySelector:(SEL)selector{ + +} + - (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod { unsigned int rv = 0; @@ -561,50 +574,52 @@ - (BOOL)hasMarkedText { - return [_markedText length] > 0; + return _markedRange.length > 0; } - (NSRange)markedRange { - if([_markedText length] > 0) - return NSMakeRange(0, [_markedText length] - 1); - return NSMakeRange(NSNotFound, 0); + return _markedRange; } - (NSRange)selectedRange { - return _selection; + return _selectedRange; } - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { + _lastKeyHandled = true; + + NSString* markedText; + if([string isKindOfClass:[NSAttributedString class]]) { - _markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string]; + markedText = [string string]; } else { - _markedText = [[NSMutableAttributedString alloc] initWithString:string]; + markedText = (NSString*) string; } - if(!_parent->InputMethod->IsActive()){ - return; + _markedRange = NSMakeRange(_selectedRange.location, [markedText length]); + + if(_parent->InputMethod->IsActive()){ + _parent->InputMethod->Client->SetPreeditText((char*)[markedText UTF8String]); } - - _parent->InputMethod->Client->SetPreeditText((char*)[_markedText.string UTF8String]); } - (void)unmarkText { - [[_markedText mutableString] setString:@""]; + if(_parent->InputMethod->IsActive()){ + _parent->InputMethod->Client->SetPreeditText(nullptr); + } - [[self inputContext] discardMarkedText]; + _markedRange = NSMakeRange(_selectedRange.location, 0); - if(!_parent->InputMethod->IsActive()){ - return; + if([self inputContext]) { + [[self inputContext] discardMarkedText]; } - - _parent->InputMethod->Client->SetPreeditText(nullptr); } - (NSArray *)validAttributesForMarkedText @@ -614,19 +629,38 @@ - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange { - return nullptr; + if(actualRange){ + range = *actualRange; + } + + NSAttributedString* subString = [_text attributedSubstringFromRange:range]; + + return subString; } - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { - [self unmarkText]; - - if(_parent != nullptr) + if(_parent == nullptr){ + return; + } + + NSString* text; + + if([string isKindOfClass:[NSAttributedString class]]) { - _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); + text = [string string]; + } + else + { + text = (NSString*) string; } - [[self inputContext] invalidateCharacterCoordinates]; + [self unmarkText]; + + uint32_t timestamp = static_cast([NSDate timeIntervalSinceReferenceDate] * 1000); + + _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(timestamp, [text UTF8String]); + } - (NSUInteger)characterIndexForPoint:(NSPoint)point @@ -746,15 +780,11 @@ } - (void) setText:(NSString *)text{ - [_text setString:text]; - - [[self inputContext] discardMarkedText]; + [[_text mutableString] setString:text]; } - (void) setSelection:(int)start :(int)end{ - _selection = NSMakeRange(start, end - start); - - [[self inputContext] invalidateCharacterCoordinates]; + _selectedRange = NSMakeRange(start, end - start); } - (void) setCursorRect:(AvnRect)rect{ @@ -766,7 +796,9 @@ _cursorRect = windowRectOnScreen; - [[self inputContext] invalidateCharacterCoordinates]; + if([self inputContext]) { + [[self inputContext] invalidateCharacterCoordinates]; + } } @end diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 40232947d9..e17bad28d7 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -212,6 +212,7 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.Headless.UnitTests"); }); Target RunRenderTests => _ => _ @@ -273,6 +274,8 @@ partial class Build : NukeBuild if(!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config, new NumergeNukeLogger())) throw new Exception("Package merge failed"); + RefAssemblyGenerator.GenerateRefAsmsInPackage(Parameters.NugetRoot / "Avalonia." + + Parameters.Version + ".nupkg"); }); Target RunTests => _ => _ diff --git a/nukebuild/Helpers.cs b/nukebuild/Helpers.cs new file mode 100644 index 0000000000..d8d06559bf --- /dev/null +++ b/nukebuild/Helpers.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; +using Nuke.Common.Utilities; + +class Helpers +{ + public static IDisposable UseTempDir(out string dir) + { + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(path); + dir = path; + return DelegateDisposable.CreateBracket(null, () => + { + try + { + Directory.Delete(path, true); + } + catch + { + // ignore + } + }); + } +} diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs new file mode 100644 index 0000000000..cbe5236bca --- /dev/null +++ b/nukebuild/RefAssemblyGenerator.cs @@ -0,0 +1,171 @@ +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using ILRepacking; +using Mono.Cecil; +using Mono.Cecil.Cil; + +public class RefAssemblyGenerator +{ + class Resolver : DefaultAssemblyResolver, IAssemblyResolver + { + private readonly string _dir; + Dictionary _cache = new(); + + public Resolver(string dir) + { + _dir = dir; + } + + public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) + { + if (_cache.TryGetValue(name.Name, out var asm)) + return asm; + var path = Path.Combine(_dir, name.Name + ".dll"); + if (File.Exists(path)) + return _cache[name.Name] = AssemblyDefinition.ReadAssembly(path, parameters); + return base.Resolve(name, parameters); + } + } + + public static void PatchRefAssembly(string file) + { + var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk"); + var snk = new byte[reader.Length]; + reader.Read(snk, 0, snk.Length); + + var def = AssemblyDefinition.ReadAssembly(file, new ReaderParameters + { + ReadWrite = true, + InMemory = true, + ReadSymbols = true, + SymbolReaderProvider = new DefaultSymbolReaderProvider(false), + AssemblyResolver = new Resolver(Path.GetDirectoryName(file)) + }); + + var obsoleteAttribute = def.MainModule.ImportReference(new TypeReference("System", "ObsoleteAttribute", def.MainModule, + def.MainModule.TypeSystem.CoreLibrary)); + var obsoleteCtor = def.MainModule.ImportReference(new MethodReference(".ctor", + def.MainModule.TypeSystem.Void, obsoleteAttribute) + { + Parameters = { new ParameterDefinition(def.MainModule.TypeSystem.String) } + }); + + foreach(var t in def.MainModule.Types) + ProcessType(t, obsoleteCtor); + def.Write(file, new WriterParameters() + { + StrongNameKeyBlob = snk, + WriteSymbols = def.MainModule.HasSymbols, + SymbolWriterProvider = new EmbeddedPortablePdbWriterProvider(), + DeterministicMvid = def.MainModule.HasSymbols + }); + } + + static void ProcessType(TypeDefinition type, MethodReference obsoleteCtor) + { + foreach (var nested in type.NestedTypes) + ProcessType(nested, obsoleteCtor); + if (type.IsInterface) + { + var hideMethods = type.Name.EndsWith("Impl") + || (type.HasCustomAttributes && type.CustomAttributes.Any(a => + a.AttributeType.FullName == "Avalonia.Metadata.PrivateApiAttribute")); + + var injectMethod = hideMethods + || type.CustomAttributes.Any(a => + a.AttributeType.FullName == "Avalonia.Metadata.NotClientImplementableAttribute"); + + if (hideMethods) + { + foreach (var m in type.Methods) + { + var dflags = MethodAttributes.Public | MethodAttributes.Family | MethodAttributes.FamORAssem | + MethodAttributes.FamANDAssem | MethodAttributes.Assembly; + m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly; + } + } + + if(injectMethod) + { + type.Methods.Add(new MethodDefinition("NotClientImplementable", + MethodAttributes.Assembly + | MethodAttributes.Abstract + | MethodAttributes.NewSlot + | MethodAttributes.HideBySig, type.Module.TypeSystem.Void)); + } + + var forceUnstable = type.CustomAttributes.Any(a => + a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute"); + + foreach (var m in type.Methods) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + foreach (var m in type.Properties) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + foreach (var m in type.Events) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + + } + } + + static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, bool force) + { + if (!force && ( + def.HasCustomAttributes == false + || def.CustomAttributes.All(a => a.AttributeType.FullName != "Avalonia.Metadata.UnstableAttribute"))) + return; + + if (def.CustomAttributes.Any(a => a.AttributeType.FullName == "System.ObsoleteAttribute")) + return; + + def.CustomAttributes.Add(new CustomAttribute(obsoleteCtor) + { + ConstructorArguments = + { + new CustomAttributeArgument(obsoleteCtor.Module.TypeSystem.String, + "This is a part of unstable API and can be changed in minor releases. You have been warned") + } + }); + } + + public static void GenerateRefAsmsInPackage(string packagePath) + { + using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), + ZipArchiveMode.Update)) + { + foreach (var entry in archive.Entries.ToList()) + { + if (entry.FullName.StartsWith("ref/")) + entry.Delete(); + } + + foreach (var entry in archive.Entries.ToList()) + { + if (entry.FullName.StartsWith("lib/") && entry.Name.EndsWith(".xml")) + { + var newEntry = archive.CreateEntry("ref/" + entry.FullName.Substring(4), + CompressionLevel.Optimal); + using (var src = entry.Open()) + using (var dst = newEntry.Open()) + src.CopyTo(dst); + } + } + + var libs = archive.Entries.Where(e => e.FullName.StartsWith("lib/") && e.FullName.EndsWith(".dll")) + .Select((e => new { s = e.FullName.Split('/'), e = e })) + .Select(e => new { Tfm = e.s[1], Name = e.s[2], Entry = e.e }) + .GroupBy(x => x.Tfm); + foreach(var tfm in libs) + using (Helpers.UseTempDir(out var temp)) + { + foreach (var l in tfm) + l.Entry.ExtractToFile(Path.Combine(temp, l.Name)); + foreach (var l in tfm) + PatchRefAssembly(Path.Combine(temp, l.Name)); + foreach (var l in tfm) + archive.CreateEntryFromFile(Path.Combine(temp, l.Name), $"ref/{l.Tfm}/{l.Name}"); + } + } + } +} \ No newline at end of file diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 13bac4b7db..d03746766e 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -31,18 +31,11 @@ - - - - - - - + + - - - + diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index e465e9caf3..877d475fb6 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -26,7 +26,7 @@ - + diff --git a/samples/ControlCatalog.NetCore/Properties/launchSettings.json b/samples/ControlCatalog.NetCore/Properties/launchSettings.json index 5964ca320e..11feb94bb3 100644 --- a/samples/ControlCatalog.NetCore/Properties/launchSettings.json +++ b/samples/ControlCatalog.NetCore/Properties/launchSettings.json @@ -6,6 +6,10 @@ "Dxgi": { "commandName": "Project", "commandLineArgs": "--dxgi" + }, + "VNC": { + "commandName": "Project", + "commandLineArgs": "--vnc" } } -} \ No newline at end of file +} diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 3b847adcbb..64bf3e53b3 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -26,8 +26,6 @@ #FFFFFFFF - #FF0078D7 - #FF005A9E diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 9c439c874f..9c511f9eb0 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -19,13 +19,9 @@ namespace ControlCatalog { public class MainView : UserControl { - private readonly IPlatformSettings _platformSettings; - public MainView() { AvaloniaXamlLoader.Load(this); - _platformSettings = AvaloniaLocator.Current.GetRequiredService(); - PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); var sideBar = this.Get("Sidebar"); @@ -141,50 +137,6 @@ namespace ControlCatalog ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; }; } - - _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; - PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); - } - - protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - base.OnDetachedFromLogicalTree(e); - - _platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged; - } - - private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e) - { - Application.Current!.Resources["SystemAccentColor"] = e.AccentColor1; - Application.Current.Resources["SystemAccentColorDark1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); - Application.Current.Resources["SystemAccentColorDark2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); - Application.Current.Resources["SystemAccentColorDark3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); - Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, 0.3); - Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, 0.5); - Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, 0.7); - - static Color ChangeColorLuminosity(Color color, double luminosityFactor) - { - var red = (double)color.R; - var green = (double)color.G; - var blue = (double)color.B; - - if (luminosityFactor < 0) - { - luminosityFactor = 1 + luminosityFactor; - red *= luminosityFactor; - green *= luminosityFactor; - blue *= luminosityFactor; - } - else if (luminosityFactor >= 0) - { - red = (255 - red) * luminosityFactor + red; - green = (255 - green) * luminosityFactor + green; - blue = (255 - blue) * luminosityFactor + blue; - } - - return new Color(color.A, (byte)red, (byte)green, (byte)blue); - } } } } diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 95378ed717..d8d9678a2d 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -170,10 +170,21 @@ - - + + + + + + + + + - + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index c9c7939c1c..39497f1811 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -270,6 +270,8 @@ namespace IntegrationTestApp this.Get("BasicListBox").SelectedIndex = -1; if (source?.Name == "MenuClickedMenuItemReset") this.Get("ClickedMenuItem").Text = "None"; + if (source?.Name == "ResetSliders") + this.Get("HorizontalSlider").Value = 50; if (source?.Name == "ShowTransparentWindow") ShowTransparentWindow(); if (source?.Name == "ShowTransparentPopup") diff --git a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj index a24e55de81..31a6b05175 100644 --- a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj +++ b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 126c488d59..fae1aacf61 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -91,7 +91,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } @@ -156,12 +156,12 @@ namespace Avalonia.Android.Platform.SkiaPlatform protected virtual void OnResized(Size size) { - Resized?.Invoke(size, PlatformResizeReason.Unspecified); + Resized?.Invoke(size, WindowResizeReason.Unspecified); } internal void Resize(Size size) { - Resized?.Invoke(size, PlatformResizeReason.Layout); + Resized?.Invoke(size, WindowResizeReason.Layout); } class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs index e350a019d4..8c731c188f 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs @@ -35,7 +35,7 @@ namespace Avalonia.Collections /// Indicates if a weak subscription should be used to track changes to the collection. /// /// A disposable used to terminate the subscription. - internal static IDisposable ForEachItem( + public static IDisposable ForEachItem( this IAvaloniaReadOnlyDictionary collection, Action added, Action removed, diff --git a/src/Avalonia.Base/Controls/IResourceDictionary.cs b/src/Avalonia.Base/Controls/IResourceDictionary.cs index 2bd1f65638..6712498bf4 100644 --- a/src/Avalonia.Base/Controls/IResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/IResourceDictionary.cs @@ -18,6 +18,6 @@ namespace Avalonia.Controls /// /// Gets a collection of merged resource dictionaries that are specifically keyed and composed to address theme scenarios. /// - IDictionary ThemeDictionaries { get; } + IDictionary ThemeDictionaries { get; } } } diff --git a/src/Avalonia.Base/Controls/IThemeVariantProvider.cs b/src/Avalonia.Base/Controls/IThemeVariantProvider.cs new file mode 100644 index 0000000000..d1dca2efbf --- /dev/null +++ b/src/Avalonia.Base/Controls/IThemeVariantProvider.cs @@ -0,0 +1,22 @@ +using Avalonia.Metadata; +using Avalonia.Styling; + +namespace Avalonia.Controls; + +/// +/// Resource provider with theme variant awareness. +/// Can be used with . +/// +/// +/// This is a helper interface for the XAML compiler to make Key property accessibly by the markup extensions. +/// Which means, it can only be used with ResourceDictionaries and markup extensions in the XAML code. +/// This API might be removed in the future minor updates. +/// +[Unstable] +public interface IThemeVariantProvider : IResourceProvider +{ + /// + /// Key property set by the compiler. + /// + ThemeVariant? Key { get; set; } +} diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 231a19baab..b928cf0672 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -13,13 +13,13 @@ namespace Avalonia.Controls /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : IResourceDictionary + public class ResourceDictionary : IResourceDictionary, IThemeVariantProvider { private object? lastDeferredItemKey; private Dictionary? _inner; private IResourceHost? _owner; private AvaloniaList? _mergedDictionaries; - private AvaloniaDictionary? _themeDictionary; + private AvaloniaDictionary? _themeDictionary; /// /// Initializes a new instance of the class. @@ -93,13 +93,13 @@ namespace Avalonia.Controls } } - public IDictionary ThemeDictionaries + public IDictionary ThemeDictionaries { get { if (_themeDictionary == null) { - _themeDictionary = new AvaloniaDictionary(2); + _themeDictionary = new AvaloniaDictionary(2); _themeDictionary.ForEachItem( (_, x) => { @@ -120,6 +120,8 @@ namespace Avalonia.Controls return _themeDictionary; } } + + ThemeVariant? IThemeVariantProvider.Key { get; set; } bool IResourceNode.HasResources { @@ -192,7 +194,7 @@ namespace Avalonia.Controls if (_themeDictionary is not null) { - IResourceProvider? themeResourceProvider; + IThemeVariantProvider? themeResourceProvider; if (theme is not null && theme != ThemeVariant.Default) { if (_themeDictionary.TryGetValue(theme, out themeResourceProvider) diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 8aed1545a5..382ebac0e3 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -119,7 +119,19 @@ namespace Avalonia.Controls resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); key = key ?? throw new ArgumentNullException(nameof(key)); - return new FloatingResourceObservable(resourceProvider, key, converter); + return new FloatingResourceObservable(resourceProvider, key, null, converter); + } + + public static IObservable GetResourceObservable( + this IResourceProvider resourceProvider, + object key, + ThemeVariant? defaultThemeVariant, + Func? converter = null) + { + resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + return new FloatingResourceObservable(resourceProvider, key, defaultThemeVariant, converter); } private class ResourceObservable : LightweightObservableBase @@ -128,7 +140,10 @@ namespace Avalonia.Controls private readonly object _key; private readonly Func? _converter; - public ResourceObservable(IResourceHost target, object key, Func? converter) + public ResourceObservable( + IResourceHost target, + object key, + Func? converter) { _target = target; _key = key; @@ -170,11 +185,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (_target is not IThemeVariantHost themeVariantHost - || !_target.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) - { - value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue; - } + var theme = (_target as IThemeVariantHost)?.ActualThemeVariant; + var value = _target.FindResource(theme, _key) ?? AvaloniaProperty.UnsetValue; return _converter?.Invoke(value) ?? value; } @@ -183,14 +195,20 @@ namespace Avalonia.Controls private class FloatingResourceObservable : LightweightObservableBase { private readonly IResourceProvider _target; + private readonly ThemeVariant? _overrideThemeVariant; private readonly object _key; private readonly Func? _converter; private IResourceHost? _owner; - public FloatingResourceObservable(IResourceProvider target, object key, Func? converter) + public FloatingResourceObservable( + IResourceProvider target, + object key, + ThemeVariant? overrideThemeVariant, + Func? converter) { _target = target; _key = key; + _overrideThemeVariant = overrideThemeVariant; _converter = converter; } @@ -233,7 +251,7 @@ namespace Avalonia.Controls { _owner.ResourcesChanged -= ResourcesChanged; } - if (_owner is IThemeVariantHost themeVariantHost) + if (_overrideThemeVariant is null && _owner is IThemeVariantHost themeVariantHost) { themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; } @@ -244,12 +262,11 @@ namespace Avalonia.Controls { _owner.ResourcesChanged += ResourcesChanged; } - if (_owner is IThemeVariantHost themeVariantHost2) + if (_overrideThemeVariant is null && _owner is IThemeVariantHost themeVariantHost2) { themeVariantHost2.ActualThemeVariantChanged -= ActualThemeVariantChanged; } - PublishNext(); } @@ -265,11 +282,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (!(_target.Owner is IThemeVariantHost themeVariantHost) - || !_target.Owner.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) - { - value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; - } + var theme = _overrideThemeVariant ?? (_target.Owner as IThemeVariantHost)?.ActualThemeVariant; + var value = _target.Owner?.FindResource(theme, _key) ?? AvaloniaProperty.UnsetValue; return _converter?.Invoke(value) ?? value; } diff --git a/src/Avalonia.Base/Input/AccessKeyHandler.cs b/src/Avalonia.Base/Input/AccessKeyHandler.cs index 13ca140565..2bd9fce947 100644 --- a/src/Avalonia.Base/Input/AccessKeyHandler.cs +++ b/src/Avalonia.Base/Input/AccessKeyHandler.cs @@ -176,7 +176,7 @@ namespace Avalonia.Input { bool menuIsOpen = MainMenu?.IsOpen == true; - if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) || menuIsOpen) + if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) && !e.KeyModifiers.HasAllFlags(KeyModifiers.Control) || menuIsOpen) { // If any other key is pressed with the Alt key held down, or the main menu is open, // find all controls who have registered that access key. diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 28a3c3aefb..beb953ce8f 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -77,14 +77,14 @@ namespace Avalonia.Input /// /// Gets the pointer position relative to a control. /// - /// The control. + /// The visual whose coordinate system to use. Pass null for toplevel coordinate system /// The pointer position in the control's coordinates. public Point GetPosition(Visual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); /// /// Returns the PointerPoint associated with the current event /// - /// The visual which coordinate system to use. Pass null for toplevel coordinate system + /// The visual whose coordinate system to use. Pass null for toplevel coordinate system /// public PointerPoint GetCurrentPoint(Visual? relativeTo) => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index e16be3fa85..94955a18ae 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -2,6 +2,7 @@ using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Avalonia.Logging; using Avalonia.Rendering; using Avalonia.Threading; @@ -64,7 +65,6 @@ namespace Avalonia.Layout } _toMeasure.Enqueue(control); - _toArrange.Enqueue(control); QueueLayoutPass(); } @@ -297,6 +297,8 @@ namespace Avalonia.Layout { control.Measure(control.PreviousMeasure.Value); } + + _toArrange.Enqueue(control); } return true; @@ -313,7 +315,10 @@ namespace Avalonia.Layout return false; } - if (control.IsMeasureValid && !control.IsArrangeValid) + if (!control.IsMeasureValid) + return false; + + if (!control.IsArrangeValid) { if (control is IEmbeddedLayoutRoot embeddedRoot) control.Arrange(new Rect(embeddedRoot.AllocatedSize)); diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index f06f272e51..7b29ec640a 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -9,8 +9,8 @@ using System; using System.Globalization; #if !BUILDTASK using Avalonia.Animation.Animators; -using static Avalonia.Utilities.SpanHelpers; #endif +using static Avalonia.Utilities.SpanHelpers; namespace Avalonia.Media { diff --git a/src/Avalonia.Base/Media/PolyLineSegment.cs b/src/Avalonia.Base/Media/PolyLineSegment.cs index 5c48c11e19..d17a621348 100644 --- a/src/Avalonia.Base/Media/PolyLineSegment.cs +++ b/src/Avalonia.Base/Media/PolyLineSegment.cs @@ -10,8 +10,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty PointsProperty - = AvaloniaProperty.Register(nameof(Points)); + public static readonly StyledProperty> PointsProperty + = AvaloniaProperty.Register>(nameof(Points)); /// /// Gets or sets the points. @@ -19,7 +19,7 @@ namespace Avalonia.Media /// /// The points. /// - public Points Points + public IList Points { get => GetValue(PointsProperty); set => SetValue(PointsProperty, value); @@ -37,9 +37,9 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The points. - public PolyLineSegment(IEnumerable points) : this() + public PolyLineSegment(IEnumerable points) { - Points.AddRange(points); + Points = new Points(points); } protected internal override void ApplyTo(StreamGeometryContext ctx) diff --git a/src/Avalonia.Base/Media/PolylineGeometry.cs b/src/Avalonia.Base/Media/PolylineGeometry.cs index dd3c298b5b..b0229b6455 100644 --- a/src/Avalonia.Base/Media/PolylineGeometry.cs +++ b/src/Avalonia.Base/Media/PolylineGeometry.cs @@ -14,8 +14,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly DirectProperty PointsProperty = - AvaloniaProperty.RegisterDirect(nameof(Points), g => g.Points, (g, f) => g.Points = f); + public static readonly DirectProperty> PointsProperty = + AvaloniaProperty.RegisterDirect>(nameof(Points), g => g.Points, (g, f) => g.Points = f); /// /// Defines the property. @@ -23,13 +23,13 @@ namespace Avalonia.Media public static readonly StyledProperty IsFilledProperty = AvaloniaProperty.Register(nameof(IsFilled)); - private Points _points; + private IList _points; private IDisposable? _pointsObserver; static PolylineGeometry() { AffectsGeometry(IsFilledProperty); - PointsProperty.Changed.AddClassHandler((s, e) => s.OnPointsChanged(e.NewValue as Points)); + PointsProperty.Changed.AddClassHandler((s, e) => s.OnPointsChanged(e.NewValue as IList)); } /// @@ -43,9 +43,9 @@ namespace Avalonia.Media /// /// Initializes a new instance of the class. /// - public PolylineGeometry(IEnumerable points, bool isFilled) : this() + public PolylineGeometry(IEnumerable points, bool isFilled) { - Points.AddRange(points); + _points = new Points(points); IsFilled = isFilled; } @@ -56,7 +56,7 @@ namespace Avalonia.Media /// The points. /// [Content] - public Points Points + public IList Points { get => _points; set => SetAndRaise(PointsProperty, ref _points, value); @@ -97,10 +97,10 @@ namespace Avalonia.Media return geometry; } - private void OnPointsChanged(Points? newValue) + private void OnPointsChanged(IList? newValue) { _pointsObserver?.Dispose(); - _pointsObserver = newValue?.ForEachItem( + _pointsObserver = (newValue as IAvaloniaList)?.ForEachItem( _ => InvalidateGeometry(), _ => InvalidateGeometry(), InvalidateGeometry); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index a382416b8a..f373e0178a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -174,7 +174,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textLine in _textLines) { - textLine.Draw(context, new Point(currentX + textLine.Start, currentY)); + textLine.Draw(context, new Point(currentX, currentY)); currentY += textLine.Height; } diff --git a/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs new file mode 100644 index 0000000000..3f60940c5e --- /dev/null +++ b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Metadata; + +[AttributeUsage(AttributeTargets.Interface)] +public sealed class PrivateApiAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/ICursorFactory.cs b/src/Avalonia.Base/Platform/ICursorFactory.cs index fff1f92d53..99a9a9d7fa 100644 --- a/src/Avalonia.Base/Platform/ICursorFactory.cs +++ b/src/Avalonia.Base/Platform/ICursorFactory.cs @@ -1,9 +1,11 @@ using Avalonia.Input; +using Avalonia.Metadata; #nullable enable namespace Avalonia.Platform { + [PrivateApi] public interface ICursorFactory { ICursorImpl GetCursor(StandardCursorType cursorType); diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index c37233d52c..b0d17f9c85 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -11,7 +11,7 @@ namespace Avalonia.Platform /// /// Defines the main platform-specific interface for the rendering subsystem. /// - [Unstable] + [Unstable, PrivateApi] public interface IPlatformRenderInterface { /// @@ -202,7 +202,7 @@ namespace Avalonia.Platform bool IsSupportedBitmapPixelFormat(PixelFormat format); } - [Unstable] + [Unstable, PrivateApi] public interface IPlatformRenderInterfaceContext : IOptionalFeatureProvider, IDisposable { /// diff --git a/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs new file mode 100644 index 0000000000..153634027c --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform.Storage.FileIO; + +namespace Avalonia.Platform.Storage; + +internal class NoopStorageProvider : BclStorageProvider +{ + public override bool CanOpen => false; + public override Task> OpenFilePickerAsync(FilePickerOpenOptions options) + { + return Task.FromResult>(Array.Empty()); + } + + public override bool CanSave => false; + public override Task SaveFilePickerAsync(FilePickerSaveOptions options) + { + return Task.FromResult(null); + } + + public override bool CanPickFolder => false; + public override Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) + { + return Task.FromResult>(Array.Empty()); + } +} diff --git a/src/Avalonia.Base/Points.cs b/src/Avalonia.Base/Points.cs index b655dbcb38..2f88ecd80f 100644 --- a/src/Avalonia.Base/Points.cs +++ b/src/Avalonia.Base/Points.cs @@ -1,6 +1,18 @@ +using System.Collections.Generic; using Avalonia.Collections; namespace Avalonia { - public sealed class Points : AvaloniaList { } + public sealed class Points : AvaloniaList + { + public Points() + { + + } + + public Points(IEnumerable points) : base(points) + { + + } + } } diff --git a/src/Avalonia.Base/Styling/IThemeVariantHost.cs b/src/Avalonia.Base/Styling/IThemeVariantHost.cs index 01583148a8..740887970b 100644 --- a/src/Avalonia.Base/Styling/IThemeVariantHost.cs +++ b/src/Avalonia.Base/Styling/IThemeVariantHost.cs @@ -7,7 +7,6 @@ namespace Avalonia.Styling; /// /// Interface for the host element with a theme variant. /// -[Unstable] public interface IThemeVariantHost : IResourceHost { /// diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 4c30e2eb2c..ccbe3baf9a 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -6,7 +6,7 @@ using Avalonia.Platform; namespace Avalonia.Threading; -[Unstable] +[PrivateApi] public interface IDispatcherImpl { bool CurrentThreadIsLoopThread { get; } @@ -19,7 +19,7 @@ public interface IDispatcherImpl void UpdateTimer(long? dueTimeInMs); } -[Unstable] +[PrivateApi] public interface IDispatcherImplWithPendingInput : IDispatcherImpl { // Checks if dispatcher implementation can @@ -28,14 +28,14 @@ public interface IDispatcherImplWithPendingInput : IDispatcherImpl bool HasPendingInput { get; } } -[Unstable] +[PrivateApi] public interface IDispatcherImplWithExplicitBackgroundProcessing : IDispatcherImpl { event Action ReadyForBackgroundProcessing; void RequestBackgroundProcessing(); } -[Unstable] +[PrivateApi] public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput { // Runs the event loop diff --git a/src/Avalonia.Base/Utilities/SpanHelpers.cs b/src/Avalonia.Base/Utilities/SpanHelpers.cs index 9a5dce9798..f80ac7c046 100644 --- a/src/Avalonia.Base/Utilities/SpanHelpers.cs +++ b/src/Avalonia.Base/Utilities/SpanHelpers.cs @@ -4,7 +4,10 @@ using System.Runtime.CompilerServices; namespace Avalonia.Utilities { - public static class SpanHelpers +#if !BUILDTASK + public +#endif + static class SpanHelpers { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParseUInt(this ReadOnlySpan span, NumberStyles style, IFormatProvider provider, out uint value) diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index e44b7290af..b654c66157 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -115,6 +115,7 @@ + diff --git a/src/Avalonia.Build.Tasks/SpanCompat.cs b/src/Avalonia.Build.Tasks/SpanCompat.cs index be59ff8b6c..00892d56e6 100644 --- a/src/Avalonia.Build.Tasks/SpanCompat.cs +++ b/src/Avalonia.Build.Tasks/SpanCompat.cs @@ -85,31 +85,7 @@ namespace System { return TrimStart().TrimEnd(); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseUInt(NumberStyles style, IFormatProvider provider, out uint value) - { - return uint.TryParse(ToString(), style, provider, out value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseInt(out int value) - { - return int.TryParse(ToString(), out value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseDouble(NumberStyles style, IFormatProvider provider, out double value) - { - return double.TryParse(ToString(), style, provider, out value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseByte(NumberStyles style, IFormatProvider provider, out byte value) - { - return byte.TryParse(ToString(), style, provider, out value); - } - + public override string ToString() => _length == 0 ? string.Empty : _s.Substring(_start, _length); internal int IndexOf(ReadOnlySpan v, StringComparison ordinal, int start = 0) diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index 64bf92b7cd..9af50180dd 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -118,6 +118,43 @@ namespace Avalonia }; } + /// + /// Begin configuring an . + /// Should only be used for testing and design purposes, as it relies on dynamic code. + /// + /// + /// Parameter from which should be created. + /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application. + /// + /// An instance. If can't be created, thrown an exception. + internal static AppBuilder Configure( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type entryPointType) + { + var appBuilderObj = entryPointType + .GetMethod( + "BuildAvaloniaApp", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy, + null, + Array.Empty(), + null)? + .Invoke(null, Array.Empty()); + + if (appBuilderObj is AppBuilder appBuilder) + { + return appBuilder; + } + + if (typeof(Application).IsAssignableFrom(entryPointType)) + { + return Configure(() => (Application)Activator.CreateInstance(entryPointType)!); + } + + throw new InvalidOperationException( + $"Unable to create AppBuilder from type {entryPointType.Name}." + + $"Input type either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application type."); + } + protected AppBuilder Self => this; public AppBuilder AfterSetup(Action callback) @@ -206,7 +243,7 @@ namespace Avalonia _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToFunc(options); }; return Self; } - + /// /// Registers an action that is executed with the current font manager. /// diff --git a/src/Avalonia.Controls/Automation/Peers/ThumbAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ThumbAutomationPeer.cs new file mode 100644 index 0000000000..1566370df0 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ThumbAutomationPeer.cs @@ -0,0 +1,12 @@ +using Avalonia.Automation.Peers; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls.Automation.Peers +{ + public class ThumbAutomationPeer : ControlAutomationPeer + { + public ThumbAutomationPeer(Thumb owner) : base(owner) { } + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Thumb; + protected override bool IsContentElementCore() => false; + } +} diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 3195c38eef..9c4bacbedf 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -16,5 +16,11 @@ + + + + + + diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index c091d07632..90153d3293 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -875,10 +875,11 @@ namespace Avalonia.Controls { if (_textBox != null) { + SetCurrentValue(TextProperty, String.Empty); + if (string.IsNullOrEmpty(Watermark) && !UseFloatingWatermark) { DateTimeFormatInfo dtfi = DateTimeHelper.GetCurrentDateFormat(); - SetCurrentValue(TextProperty, string.Empty); _defaultText = string.Empty; var watermarkFormat = "<{0}>"; string watermarkText; diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 3a4ae80cf4..387357dddd 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls.Embedding.Offscreen set { _clientSize = value; - Resized?.Invoke(value, PlatformResizeReason.Unspecified); + Resized?.Invoke(value, WindowResizeReason.Unspecified); } } @@ -65,7 +65,7 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action? Input { get; set; } public Action? Paint { get; set; } - public Action? Resized { get; set; } + public Action? Resized { get; set; } public Action? ScalingChanged { get; set; } public Action? TransparencyLevelChanged { get; set; } diff --git a/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs b/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs index 4c844ce30f..2ff74cc582 100644 --- a/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs +++ b/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs @@ -3,7 +3,7 @@ using Avalonia.Metadata; namespace Avalonia.Platform { - [Unstable] + [Unstable, PrivateApi] public interface IPlatformIconLoader { IWindowIconImpl LoadIcon(string fileName); diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 29156f4030..bb6b2304af 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -9,40 +9,6 @@ using Avalonia.Rendering; namespace Avalonia.Platform { - /// - /// Describes the reason for a message. - /// - public enum PlatformResizeReason - { - /// - /// The resize reason is unknown or unspecified. - /// - Unspecified, - - /// - /// The resize was due to the user resizing the window, for example by dragging the - /// window frame. - /// - User, - - /// - /// The resize was initiated by the application, for example by setting one of the sizing- - /// related properties on such as or - /// . - /// - Application, - - /// - /// The resize was initiated by the layout system. - /// - Layout, - - /// - /// The resize was due to a change in DPI. - /// - DpiChange, - } - /// /// Defines a platform-specific top-level window implementation. /// @@ -93,7 +59,7 @@ namespace Avalonia.Platform /// /// Gets or sets a method called when the toplevel is resized. /// - Action? Resized { get; set; } + Action? Resized { get; set; } /// /// Gets or sets a method called when the toplevel's scaling changes. diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 31b144ce00..5591e68235 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -114,7 +114,7 @@ namespace Avalonia.Platform /// /// The new client size. /// The reason for the resize. - void Resize(Size clientSize, PlatformResizeReason reason = PlatformResizeReason.Application); + void Resize(Size clientSize, WindowResizeReason reason = WindowResizeReason.Application); /// /// Sets the client size of the top level. diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index 5acc5adccd..f6cf8c604e 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -2,7 +2,7 @@ using Avalonia.Metadata; namespace Avalonia.Platform { - [Unstable] + [Unstable, PrivateApi] public interface IWindowingPlatform { IWindowImpl CreateWindow(); diff --git a/src/Avalonia.Controls/Platform/ScreenHelper.cs b/src/Avalonia.Controls/Platform/ScreenHelper.cs index 0bd2be69d0..59b29b4748 100644 --- a/src/Avalonia.Controls/Platform/ScreenHelper.cs +++ b/src/Avalonia.Controls/Platform/ScreenHelper.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; +using Avalonia.Controls; using Avalonia.Utilities; #nullable enable namespace Avalonia.Platform { - public static class ScreenHelper + internal static class ScreenHelper { public static Screen? ScreenFromPoint(PixelPoint point, IReadOnlyList screens) { diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index c205830bc2..9854bdbea6 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -1,4 +1,6 @@ using System; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Interactivity; @@ -45,6 +47,8 @@ namespace Avalonia.Controls.Primitives remove { RemoveHandler(DragCompletedEvent, value); } } + protected override AutomationPeer OnCreateAutomationPeer() => new ThumbAutomationPeer(this); + protected virtual void OnDragStarted(VectorEventArgs e) { } @@ -81,7 +85,7 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { - var point = e.GetPosition(this.GetVisualParent()); + var point = e.GetPosition(null); var ev = new VectorEventArgs { RoutedEvent = DragDeltaEvent, @@ -96,7 +100,7 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerPressed(PointerPressedEventArgs e) { e.Handled = true; - _lastPoint = e.GetPosition(this.GetVisualParent()); + _lastPoint = e.GetPosition(null); var ev = new VectorEventArgs { @@ -119,7 +123,7 @@ namespace Avalonia.Controls.Primitives var ev = new VectorEventArgs { RoutedEvent = DragCompletedEvent, - Vector = (Vector)e.GetPosition(this.GetVisualParent()), + Vector = (Vector)e.GetPosition(null), }; RaiseEvent(ev); diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 8dc19eb1d4..530f28fbb6 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -38,7 +38,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty PullDirectionProperty = + internal static readonly StyledProperty PullDirectionProperty = AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); /// diff --git a/src/Avalonia.Controls/Shapes/Polygon.cs b/src/Avalonia.Controls/Shapes/Polygon.cs index 70a45f3516..78def84448 100644 --- a/src/Avalonia.Controls/Shapes/Polygon.cs +++ b/src/Avalonia.Controls/Shapes/Polygon.cs @@ -13,10 +13,15 @@ namespace Avalonia.Controls.Shapes AffectsGeometry(PointsProperty); } + public Polygon() + { + Points = new Points(); + } + public IList Points { - get { return GetValue(PointsProperty); } - set { SetValue(PointsProperty, value); } + get => GetValue(PointsProperty); + set => SetValue(PointsProperty, value); } protected override Geometry CreateDefiningGeometry() diff --git a/src/Avalonia.Controls/Shapes/Polyline.cs b/src/Avalonia.Controls/Shapes/Polyline.cs index 4b4bb3ffd0..2533794f89 100644 --- a/src/Avalonia.Controls/Shapes/Polyline.cs +++ b/src/Avalonia.Controls/Shapes/Polyline.cs @@ -14,10 +14,15 @@ namespace Avalonia.Controls.Shapes AffectsGeometry(PointsProperty); } + public Polyline() + { + Points = new Points(); + } + public IList Points { - get { return GetValue(PointsProperty); } - set { SetValue(PointsProperty, value); } + get => GetValue(PointsProperty); + set => SetValue(PointsProperty, value); } protected override Geometry CreateDefiningGeometry() diff --git a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs index 64a883e88c..895ce53907 100644 --- a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs @@ -18,10 +18,10 @@ namespace Avalonia.Controls.Templates { } - public new ControlTemplateResult Build(TemplatedControl param) + public new TemplateResult Build(TemplatedControl param) { var (control, scope) = BuildWithNameScope(param); - return new ControlTemplateResult(control, scope); + return new(control, scope); } } } diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs index 38ad6561ab..c3f9c9e8aa 100644 --- a/src/Avalonia.Controls/Templates/IControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/IControlTemplate.cs @@ -5,23 +5,7 @@ namespace Avalonia.Controls.Templates /// /// Interface representing a template used to build a . /// - public interface IControlTemplate : ITemplate + public interface IControlTemplate : ITemplate?> { } - - public class ControlTemplateResult : TemplateResult - { - public Control Control { get; } - - public ControlTemplateResult(Control control, INameScope nameScope) : base(control, nameScope) - { - Control = control; - } - - public new void Deconstruct(out Control control, out INameScope scope) - { - control = Control; - scope = NameScope; - } - } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 07b1e9b51f..8a1cdf3f80 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -432,10 +432,13 @@ namespace Avalonia.Controls IStyleHost IStyleHost.StylingParent => _globalStyles!; + /// + /// File System storage service used for file pickers and bookmarks. + /// public IStorageProvider StorageProvider => _storageProvider ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) ?? PlatformImpl?.TryGetFeature() - ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); + ?? new NoopStorageProvider(); public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature(); @@ -569,7 +572,7 @@ namespace Avalonia.Controls /// /// The new client size. /// The reason for the resize. - protected virtual void HandleResized(Size clientSize, PlatformResizeReason reason) + internal virtual void HandleResized(Size clientSize, WindowResizeReason reason) { ClientSize = clientSize; FrameSize = PlatformImpl!.FrameSize; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index f9593f1c1b..48edb81b16 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -227,7 +227,7 @@ namespace Avalonia.Controls impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; - this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application)); + this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, WindowResizeReason.Application)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); } @@ -700,7 +700,7 @@ namespace Avalonia.Controls if (initialSize != ClientSize) { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + PlatformImpl?.Resize(initialSize, WindowResizeReason.Layout); } LayoutManager.ExecuteInitialLayoutPass(); @@ -778,7 +778,7 @@ namespace Avalonia.Controls if (initialSize != ClientSize) { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + PlatformImpl?.Resize(initialSize, WindowResizeReason.Layout); } LayoutManager.ExecuteInitialLayoutPass(); @@ -975,7 +975,7 @@ namespace Avalonia.Controls protected sealed override Size ArrangeSetBounds(Size size) { - PlatformImpl?.Resize(size, PlatformResizeReason.Layout); + PlatformImpl?.Resize(size, WindowResizeReason.Layout); return ClientSize; } @@ -994,7 +994,7 @@ namespace Avalonia.Controls } /// - protected sealed override void HandleResized(Size clientSize, PlatformResizeReason reason) + internal override void HandleResized(Size clientSize, WindowResizeReason reason) { if (ClientSize != clientSize || double.IsNaN(Width) || double.IsNaN(Height)) { @@ -1005,8 +1005,8 @@ namespace Avalonia.Controls // to the requested size. if (sizeToContent != SizeToContent.Manual && CanResize && - reason == PlatformResizeReason.Unspecified || - reason == PlatformResizeReason.User) + reason == WindowResizeReason.Unspecified || + reason == WindowResizeReason.User) { if (clientSize.Width != ClientSize.Width) sizeToContent &= ~SizeToContent.Width; diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 814a9b5960..c2523207e4 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -80,6 +80,11 @@ namespace Avalonia.Controls /// public event EventHandler? PositionChanged; + /// + /// Occurs when the window is resized. + /// + public event EventHandler? Resized; + public new IWindowBaseImpl? PlatformImpl => (IWindowBaseImpl?) base.PlatformImpl; /// @@ -155,6 +160,15 @@ namespace Avalonia.Controls } } + /// + /// Trys to get the platform handle for the window. + /// + /// + /// An describing the window handle, or null if the handle + /// could not be retrieved. + /// + public IPlatformHandle? TryGetPlatformHandle() => PlatformImpl?.Handle; + /// /// Ensures that the window is initialized. /// @@ -188,6 +202,12 @@ namespace Avalonia.Controls base.OnOpened(e); } + /// + /// Raises the event. + /// + /// An that contains the event data. + protected virtual void OnResized(WindowResizedEventArgs e) => Resized?.Invoke(this, e); + protected override void HandleClosed() { using (FreezeVisibilityChangeHandling()) @@ -208,7 +228,7 @@ namespace Avalonia.Controls /// /// The new client size. /// The reason for the resize. - protected override void HandleResized(Size clientSize, PlatformResizeReason reason) + internal override void HandleResized(Size clientSize, WindowResizeReason reason) { FrameSize = PlatformImpl?.FrameSize; @@ -218,6 +238,8 @@ namespace Avalonia.Controls LayoutManager.ExecuteLayoutPass(); Renderer.Resized(clientSize); } + + OnResized(new WindowResizedEventArgs(clientSize, reason)); } /// diff --git a/src/Avalonia.Controls/WindowResizedEventArgs.cs b/src/Avalonia.Controls/WindowResizedEventArgs.cs new file mode 100644 index 0000000000..daa8aa0f09 --- /dev/null +++ b/src/Avalonia.Controls/WindowResizedEventArgs.cs @@ -0,0 +1,61 @@ +using System; +using Avalonia.Layout; + +namespace Avalonia.Controls +{ + /// + /// Describes the reason for a event. + /// + public enum WindowResizeReason + { + /// + /// The resize reason is unknown or unspecified. + /// + Unspecified, + + /// + /// The resize was due to the user resizing the window, for example by dragging the + /// window frame. + /// + User, + + /// + /// The resize was initiated by the application, for example by setting one of the sizing- + /// related properties on such as or + /// . + /// + Application, + + /// + /// The resize was initiated by the layout system. + /// + Layout, + + /// + /// The resize was due to a change in DPI. + /// + DpiChange, + } + + /// + /// Provides data for the event. + /// + public class WindowResizedEventArgs : EventArgs + { + internal WindowResizedEventArgs(Size clientSize, WindowResizeReason reason) + { + ClientSize = clientSize; + Reason = reason; + } + + /// + /// Gets the new client size of the window in device-independent pixels. + /// + public Size ClientSize { get; } + + /// + /// Gets the reason for the resize. + /// + public WindowResizeReason Reason { get; } + } +} diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 2da8f38ea9..e0fcf8e530 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -59,7 +59,7 @@ namespace Avalonia.DesignerSupport.Remote base.OnMessage(transport, obj); } - public void Resize(Size clientSize, PlatformResizeReason reason) + public void Resize(Size clientSize, WindowResizeReason reason) { _transport.Send(new RequestViewportResizeMessage { diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 85605ccd9d..313063269b 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -179,17 +179,9 @@ namespace Avalonia.DesignerSupport.Remote var entryPoint = asm.EntryPoint; if (entryPoint == null) throw Die($"Assembly {args.AppPath} doesn't have an entry point"); - var builderMethod = entryPoint.DeclaringType.GetMethod( - BuilderMethodName, - BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy, - null, - Array.Empty(), - null); - if (builderMethod == null) - throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}"); + Log($"Obtaining AppBuilder instance from {entryPoint.DeclaringType!.FullName}"); + var appBuilder = AppBuilder.Configure(entryPoint.DeclaringType); Design.IsDesignMode = true; - Log($"Obtaining AppBuilder instance from {builderMethod.DeclaringType.FullName}.{builderMethod.Name}"); - var appBuilder = builderMethod.Invoke(null, null); Log($"Initializing application in design mode"); var initializer =(IAppInitializer)Activator.CreateInstance(typeof(AppInitializer)); transport = initializer.ConfigureApp(transport, args, appBuilder); diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index ea427e4c92..f6f5c185e9 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -32,7 +32,7 @@ namespace Avalonia.DesignerSupport.Remote public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Func Closing { get; set; } public Action Closed { get; set; } @@ -59,7 +59,7 @@ namespace Avalonia.DesignerSupport.Remote PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, (_, size, __) => { - Resize(size, PlatformResizeReason.Unspecified); + Resize(size, WindowResizeReason.Unspecified); })); } @@ -112,7 +112,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void Resize(Size clientSize, PlatformResizeReason reason) + public void Resize(Size clientSize, WindowResizeReason reason) { } diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index a25bb68458..8b2b38bb82 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Avalonia.Logging; +using Avalonia.Media; using Avalonia.Platform; using Tmds.DBus.SourceGenerator; @@ -9,7 +10,10 @@ namespace Avalonia.FreeDesktop internal class DBusPlatformSettings : DefaultPlatformSettings { private readonly OrgFreedesktopPortalSettings? _settings; + private PlatformColorValues? _lastColorValues; + private PlatformThemeVariant? _themeVariant; + private Color? _accentColor; public DBusPlatformSettings() { @@ -21,24 +25,33 @@ namespace Avalonia.FreeDesktop _ = TryGetInitialValueAsync(); } - public override PlatformColorValues GetColorValues() - { - return _lastColorValues ?? base.GetColorValues(); - } + public override PlatformColorValues GetColorValues() => _lastColorValues ?? base.GetColorValues(); private async Task TryGetInitialValueAsync() { try { var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); - _lastColorValues = GetColorValuesFromSetting(value); - OnColorValuesChanged(_lastColorValues); + _themeVariant = ReadAsColorScheme(value); + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.freedesktop.appearance.color-scheme value", ex); + } + + try + { + var value = await _settings!.ReadAsync("org.kde.kdeglobals.General", "AccentColor"); + _accentColor = ReadAsAccentColor(value); } catch (Exception ex) { - _lastColorValues = base.GetColorValues(); - Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get setting value", ex); + Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.kde.kdeglobals.General.AccentColor value", ex); } + + _lastColorValues = BuildPlatformColorValues(); + if (_lastColorValues is not null) + OnColorValuesChanged(_lastColorValues); } private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple) @@ -46,25 +59,48 @@ namespace Avalonia.FreeDesktop if (exception is not null) return; - if (valueTuple is ("org.freedesktop.appearance", "color-scheme", { } value)) + switch (valueTuple) { - /* - 0: No preference - 1: Prefer dark appearance - 2: Prefer light appearance - */ - _lastColorValues = GetColorValuesFromSetting(value); - OnColorValuesChanged(_lastColorValues); + case ("org.freedesktop.appearance", "color-scheme", { } colorScheme): + _themeVariant = ReadAsColorScheme(colorScheme); + _lastColorValues = BuildPlatformColorValues(); + OnColorValuesChanged(_lastColorValues!); + break; + case ("org.kde.kdeglobals.General", "AccentColor", { } accentColor): + _accentColor = ReadAsAccentColor(accentColor); + _lastColorValues = BuildPlatformColorValues(); + OnColorValuesChanged(_lastColorValues!); + break; } } - private static PlatformColorValues GetColorValuesFromSetting(DBusVariantItem value) + private PlatformColorValues? BuildPlatformColorValues() + { + if (_themeVariant is { } themeVariant && _accentColor is { } accentColor) + return new PlatformColorValues { ThemeVariant = themeVariant, AccentColor1 = accentColor }; + if (_themeVariant is { } themeVariant1) + return new PlatformColorValues { ThemeVariant = themeVariant1 }; + if (_accentColor is { } accentColor1) + return new PlatformColorValues { AccentColor1 = accentColor1 }; + return null; + } + + private static PlatformThemeVariant ReadAsColorScheme(DBusVariantItem value) { + /* + 0: No preference + 1: Prefer dark appearance + 2: Prefer light appearance + */ var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1; - return new PlatformColorValues - { - ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light - }; + return isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light; + } + + private static Color ReadAsAccentColor(DBusVariantItem value) + { + var colorStr = ((value.Value as DBusVariantItem)!.Value as DBusStringItem)!.Value; + var rgb = colorStr.Split(','); + return new Color(255, byte.Parse(rgb[0]), byte.Parse(rgb[1]), byte.Parse(rgb[2])); } } } diff --git a/src/Avalonia.Headless/Avalonia.Headless.csproj b/src/Avalonia.Headless/Avalonia.Headless.csproj deleted file mode 100644 index 95f7b79009..0000000000 --- a/src/Avalonia.Headless/Avalonia.Headless.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net6.0;netstandard2.0 - - - - - - - - - - diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 095662a538..e69c39a41e 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -26,8 +26,4 @@ - - - - diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 0953527284..6b7f7e8883 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Native.Interop; using Avalonia.Platform; @@ -29,7 +30,7 @@ namespace Avalonia.Native private void MoveResize(PixelPoint position, Size size, double scaling) { Position = position; - Resize(size, PlatformResizeReason.Layout); + Resize(size, WindowResizeReason.Layout); //TODO: We ignore the scaling override for now } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 0dff46057e..26c3da9d50 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -95,7 +95,7 @@ namespace Avalonia.Native var monitor = Screen.AllScreens.OrderBy(x => x.Scaling) .FirstOrDefault(m => m.Bounds.Contains(Position)); - Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d), PlatformResizeReason.Layout); + Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d), WindowResizeReason.Layout); } public IAvnWindowBase Native => _native; @@ -160,7 +160,7 @@ namespace Avalonia.Native public Action LostFocus { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice => _mouse; public abstract IPopupImpl CreatePopup(); @@ -211,7 +211,7 @@ namespace Avalonia.Native { var s = new Size(size->Width, size->Height); _parent._savedLogicalSize = s; - _parent.Resized?.Invoke(s, (PlatformResizeReason)reason); + _parent.Resized?.Invoke(s, (WindowResizeReason)reason); } } @@ -360,7 +360,7 @@ namespace Avalonia.Native } } - public void Resize(Size clientSize, PlatformResizeReason reason) + public void Resize(Size clientSize, WindowResizeReason reason) { _native?.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason); } diff --git a/src/Avalonia.Themes.Fluent/Accents/AccentColors.xaml b/src/Avalonia.Themes.Fluent/Accents/AccentColors.xaml deleted file mode 100644 index 0fb3ab73c2..0000000000 --- a/src/Avalonia.Themes.Fluent/Accents/AccentColors.xaml +++ /dev/null @@ -1,12 +0,0 @@ - - - - #FF0078D7 - #FF005A9E - #FF004275 - #FF002642 - #FF429CE3 - #FF76B9ED - #FFA6D8FF - diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml new file mode 100644 index 0000000000..362d543646 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml @@ -0,0 +1,69 @@ + + + + + #FFFFFFFF + #33FFFFFF + #99FFFFFF + #CCFFFFFF + #66FFFFFF + #FF000000 + #33000000 + #99000000 + #CC000000 + #66000000 + #FF171717 + #FF000000 + #33000000 + #66000000 + #CC000000 + #FFCCCCCC + #FF7A7A7A + #FFCCCCCC + #FFF2F2F2 + #FFE6E6E6 + #FFF2F2F2 + #FFFFFFFF + #FF767676 + #19000000 + #33000000 + #C50500 + #FFFFFFFF + #17000000 + #2E000000 + + + #FF000000 + #33000000 + #99000000 + #CC000000 + #66000000 + #FFFFFFFF + #33FFFFFF + #99FFFFFF + #CCFFFFFF + #66FFFFFF + #FFF2F2F2 + #FF000000 + #33000000 + #66000000 + #CC000000 + #FF333333 + #FF858585 + #FF767676 + #FF171717 + #FF1F1F1F + #FF2B2B2B + #FFFFFFFF + #FF767676 + #19FFFFFF + #33FFFFFF + #FFF000 + #FF000000 + #18FFFFFF + #30FFFFFF + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml similarity index 78% rename from src/Avalonia.Themes.Fluent/Accents/Base.xaml rename to src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml index c19a4f5c09..517a80fd7e 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="using:System" xmlns:converters="using:Avalonia.Controls.Converters"> - fonts:Inter#Inter, $Default 14 @@ -28,39 +27,33 @@ - - - - #FFFFFFFF - #33FFFFFF - #99FFFFFF - #CCFFFFFF - #66FFFFFF - #FF000000 - #33000000 - #99000000 - #CC000000 - #66000000 - #FF171717 - #FF000000 - #33000000 - #66000000 - #CC000000 - #FFCCCCCC - #FF7A7A7A - #FFCCCCCC - #FFF2F2F2 - #FFE6E6E6 - #FFF2F2F2 - #FFFFFFFF - #FF767676 - #19000000 - #33000000 - #C50500 + 374 + 0,2,0,2 + 1 + -1,0,-1,0 + 32 + 64 + 456 + 0 + 1 + 0 + + 12,11,12,12 + 96 + 40 + 758 - #17000000 - #2E000000 + + 0 + + 0,4,0,4 + + + 12,0,12,0 + + + - - - - - - - - #FFFFFFFF - - - - 374 - 0,2,0,2 - 1 - -1,0,-1,0 - 32 - 64 - 456 - 0 - 1 - 0 - - 12,11,12,12 - 96 - 40 - 758 - - - 0 - - - 0,4,0,4 - - - 12,0,12,0 + - - #FF000000 - #33000000 - #99000000 - #CC000000 - #66000000 - #FFFFFFFF - #33FFFFFF - #99FFFFFF - #CCFFFFFF - #66FFFFFF - #FFF2F2F2 - #FF000000 - #33000000 - #66000000 - #CC000000 - #FF333333 - #FF858585 - #FF767676 - #FF171717 - #FF1F1F1F - #FF2B2B2B - #FFFFFFFF - #FF767676 - #19FFFFFF - #33FFFFFF - #FFF000 - - #18FFFFFF - #30FFFFFF - - - - - - - - #FF000000 - - - 374 - 0,2,0,2 - 1 - -1,0,-1,0 - 32 - 64 - 456 - 0 - 1 - 0 - - 12,11,12,12 - 96 - 40 - 758 - - - 0 - - - 0,4,0,4 - - - 12,0,12,0 + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml index a9bc622221..61a74f26a4 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml @@ -4,8 +4,8 @@ - - + + @@ -52,7 +52,8 @@ - + @@ -291,15 +292,17 @@ ResourceKey="SystemControlHighlightBaseHighBrush" /> - + - - + + @@ -309,13 +312,17 @@ ResourceKey="SystemControlBackgroundBaseMediumLowBrush" /> - - + + - - + + - - + + - - + + @@ -470,8 +481,8 @@ - - + + @@ -502,8 +513,8 @@ - - + + @@ -701,8 +712,9 @@ - - + + @@ -775,8 +787,8 @@ - - + + @@ -823,7 +835,8 @@ - + @@ -1065,14 +1078,17 @@ ResourceKey="SystemControlHighlightBaseHighBrush" /> - - + + - - + + @@ -1082,13 +1098,17 @@ ResourceKey="SystemControlBackgroundBaseMediumLowBrush" /> - - + + - - + + - - + + - - + + @@ -1243,8 +1267,8 @@ - - + + @@ -1275,12 +1299,12 @@ - - + + - - + + @@ -1476,8 +1500,8 @@ - - + + diff --git a/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs b/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs new file mode 100644 index 0000000000..a4ef15f950 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs @@ -0,0 +1,163 @@ +using System; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Styling; + +namespace Avalonia.Themes.Fluent.Accents; + +internal class SystemAccentColors : IResourceProvider +{ + public const string AccentKey = "SystemAccentColor"; + public const string AccentDark1Key = "SystemAccentColorDark1"; + public const string AccentDark2Key = "SystemAccentColorDark2"; + public const string AccentDark3Key = "SystemAccentColorDark3"; + public const string AccentLight1Key = "SystemAccentColorLight1"; + public const string AccentLight2Key = "SystemAccentColorLight2"; + public const string AccentLight3Key = "SystemAccentColorLight3"; + + private static readonly Color s_defaultSystemAccentColor = Color.FromRgb(0, 120, 215); + private readonly IPlatformSettings? _platformSettings; + private bool _invalidateColors = true; + private Color _systemAccentColor; + private Color _systemAccentColorDark1, _systemAccentColorDark2, _systemAccentColorDark3; + private Color _systemAccentColorLight1, _systemAccentColorLight2, _systemAccentColorLight3; + + public SystemAccentColors() + { + _platformSettings = AvaloniaLocator.Current.GetService(); + } + + public bool HasResources => true; + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) + { + if (key is string strKey) + { + if (strKey.Equals(AccentKey, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColor; + return true; + } + + if (strKey.Equals(AccentDark1Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorDark1; + return true; + } + + if (strKey.Equals(AccentDark2Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorDark2; + return true; + } + + if (strKey.Equals(AccentDark3Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorDark3; + return true; + } + + if (strKey.Equals(AccentLight1Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorLight1; + return true; + } + + if (strKey.Equals(AccentLight2Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorLight2; + return true; + } + + if (strKey.Equals(AccentLight3Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorLight3; + return true; + } + } + + value = null; + return false; + } + + public IResourceHost? Owner { get; private set; } + public event EventHandler? OwnerChanged; + public void AddOwner(IResourceHost owner) + { + if (Owner != owner) + { + Owner = owner; + OwnerChanged?.Invoke(this, EventArgs.Empty); + + if (_platformSettings is not null) + { + _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; + } + } + } + + public void RemoveOwner(IResourceHost owner) + { + if (Owner == owner) + { + Owner = null; + OwnerChanged?.Invoke(this, EventArgs.Empty); + + if (_platformSettings is not null) + { + _platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged; + } + } + } + + private void EnsureColors() + { + if (_invalidateColors) + { + _invalidateColors = false; + + _systemAccentColor = _platformSettings?.GetColorValues().AccentColor1 ?? s_defaultSystemAccentColor; + (_systemAccentColorDark1,_systemAccentColorDark2, _systemAccentColorDark3, + _systemAccentColorLight1, _systemAccentColorLight2, _systemAccentColorLight3) = CalculateAccentShades(_systemAccentColor); + } + } + + public static (Color d1, Color d2, Color d3, Color l1, Color l2, Color l3) CalculateAccentShades(Color accentColor) + { + // dark1step = (hslAccent.L - SystemAccentColorDark1.L) * 255 + const double dark1step = 28.5 / 255d; + const double dark2step = 49 / 255d; + const double dark3step = 74.5 / 255d; + // light1step = (SystemAccentColorLight1.L - hslAccent.L) * 255 + const double light1step = 39 / 255d; + const double light2step = 70 / 255d; + const double light3step = 103 / 255d; + + var hslAccent = accentColor.ToHsl(); + + return ( + // Darker shades + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L - dark1step).ToRgb(), + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L - dark2step).ToRgb(), + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L - dark3step).ToRgb(), + + // Lighter shades + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L + light1step).ToRgb(), + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L + light2step).ToRgb(), + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L + light3step).ToRgb() + ); + } + + private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e) + { + _invalidateColors = true; + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } +} diff --git a/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs b/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs new file mode 100644 index 0000000000..366af8e227 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs @@ -0,0 +1,158 @@ +using Avalonia.Media; + +namespace Avalonia.Themes.Fluent; + +public partial class ColorPaletteResources +{ + private bool _hasAccentColor; + private Color _accentColor; + private Color _accentColorDark1, _accentColorDark2, _accentColorDark3; + private Color _accentColorLight1, _accentColorLight2, _accentColorLight3; + + public static readonly DirectProperty AccentProperty + = AvaloniaProperty.RegisterDirect(nameof(Accent), r => r.Accent, (r, v) => r.Accent = v); + + /// + /// Gets or sets the Accent color value. + /// + public Color Accent + { + get => _accentColor; + set => SetAndRaise(AccentProperty, ref _accentColor, value); + } + + /// + /// Gets or sets the AltHigh color value. + /// + public Color AltHigh { get => GetColor("SystemAltHighColor"); set => SetColor("SystemAltHighColor", value); } + + /// + /// Gets or sets the AltLow color value. + /// + public Color AltLow { get => GetColor("SystemAltLowColor"); set => SetColor("SystemAltLowColor", value); } + + /// + /// Gets or sets the AltMedium color value. + /// + public Color AltMedium { get => GetColor("SystemAltMediumColor"); set => SetColor("SystemAltMediumColor", value); } + + /// + /// Gets or sets the AltMediumHigh color value. + /// + public Color AltMediumHigh { get => GetColor("SystemAltMediumHighColor"); set => SetColor("SystemAltMediumHighColor", value); } + + /// + /// Gets or sets the AltMediumLow color value. + /// + public Color AltMediumLow { get => GetColor("SystemAltMediumLowColor"); set => SetColor("SystemAltMediumLowColor", value); } + + /// + /// Gets or sets the BaseHigh color value. + /// + public Color BaseHigh { get => GetColor("SystemBaseHighColor"); set => SetColor("SystemBaseHighColor", value); } + + /// + /// Gets or sets the BaseLow color value. + /// + public Color BaseLow { get => GetColor("SystemBaseLowColor"); set => SetColor("SystemBaseLowColor", value); } + + /// + /// Gets or sets the BaseMedium color value. + /// + public Color BaseMedium { get => GetColor("SystemBaseMediumColor"); set => SetColor("SystemBaseMediumColor", value); } + + /// + /// Gets or sets the BaseMediumHigh color value. + /// + public Color BaseMediumHigh { get => GetColor("SystemBaseMediumHighColor"); set => SetColor("SystemBaseMediumHighColor", value); } + + /// + /// Gets or sets the BaseMediumLow color value. + /// + public Color BaseMediumLow { get => GetColor("SystemBaseMediumLowColor"); set => SetColor("SystemBaseMediumLowColor", value); } + + /// + /// Gets or sets the ChromeAltLow color value. + /// + public Color ChromeAltLow { get => GetColor("SystemChromeAltLowColor"); set => SetColor("SystemChromeAltLowColor", value); } + + /// + /// Gets or sets the ChromeBlackHigh color value. + /// + public Color ChromeBlackHigh { get => GetColor("SystemChromeBlackHighColor"); set => SetColor("SystemChromeBlackHighColor", value); } + + /// + /// Gets or sets the ChromeBlackLow color value. + /// + public Color ChromeBlackLow { get => GetColor("SystemChromeBlackLowColor"); set => SetColor("SystemChromeBlackLowColor", value); } + + /// + /// Gets or sets the ChromeBlackMedium color value. + /// + public Color ChromeBlackMedium { get => GetColor("SystemChromeBlackMediumColor"); set => SetColor("SystemChromeBlackMediumColor", value); } + + /// + /// Gets or sets the ChromeBlackMediumLow color value. + /// + public Color ChromeBlackMediumLow { get => GetColor("SystemChromeBlackMediumLowColor"); set => SetColor("SystemChromeBlackMediumLowColor", value); } + + /// + /// Gets or sets the ChromeDisabledHigh color value. + /// + public Color ChromeDisabledHigh { get => GetColor("SystemChromeDisabledHighColor"); set => SetColor("SystemChromeDisabledHighColor", value); } + + /// + /// Gets or sets the ChromeDisabledLow color value. + /// + public Color ChromeDisabledLow { get => GetColor("SystemChromeDisabledLowColor"); set => SetColor("SystemChromeDisabledLowColor", value); } + + /// + /// Gets or sets the ChromeGray color value. + /// + public Color ChromeGray { get => GetColor("SystemChromeGrayColor"); set => SetColor("SystemChromeGrayColor", value); } + + /// + /// Gets or sets the ChromeHigh color value. + /// + public Color ChromeHigh { get => GetColor("SystemChromeHighColor"); set => SetColor("SystemChromeHighColor", value); } + + /// + /// Gets or sets the ChromeLow color value. + /// + public Color ChromeLow { get => GetColor("SystemChromeLowColor"); set => SetColor("SystemChromeLowColor", value); } + + /// + /// Gets or sets the ChromeMedium color value. + /// + public Color ChromeMedium { get => GetColor("SystemChromeMediumColor"); set => SetColor("SystemChromeMediumColor", value); } + + /// + /// Gets or sets the ChromeMediumLow color value. + /// + public Color ChromeMediumLow { get => GetColor("SystemChromeMediumLowColor"); set => SetColor("SystemChromeMediumLowColor", value); } + + /// + /// Gets or sets the ChromeWhite color value. + /// + public Color ChromeWhite { get => GetColor("SystemChromeWhiteColor"); set => SetColor("SystemChromeWhiteColor", value); } + + /// + /// Gets or sets the ErrorText color value. + /// + public Color ErrorText { get => GetColor("SystemErrorTextColor"); set => SetColor("SystemErrorTextColor", value); } + + /// + /// Gets or sets the ListLow color value. + /// + public Color ListLow { get => GetColor("SystemListLowColor"); set => SetColor("SystemListLowColor", value); } + + /// + /// Gets or sets the ListMedium color value. + /// + public Color ListMedium { get => GetColor("SystemListMediumColor"); set => SetColor("SystemListMediumColor", value); } + + /// + /// Gets or sets the RegionColor color value. + /// + public Color RegionColor { get => GetColor("SystemRegionColor"); set => SetColor("SystemRegionColor", value); } +} diff --git a/src/Avalonia.Themes.Fluent/ColorPaletteResources.cs b/src/Avalonia.Themes.Fluent/ColorPaletteResources.cs new file mode 100644 index 0000000000..ce52f51752 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ColorPaletteResources.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.Themes.Fluent.Accents; + +namespace Avalonia.Themes.Fluent; + +/// +/// Represents a specialized resource dictionary that contains color resources used by FluentTheme elements. +/// +/// +/// This class can only be used in . +/// +public partial class ColorPaletteResources : AvaloniaObject, IResourceNode +{ + private readonly Dictionary _colors = new(StringComparer.InvariantCulture); + + public bool HasResources => _hasAccentColor || _colors.Count > 0; + + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) + { + if (key is string strKey) + { + if (strKey.Equals(SystemAccentColors.AccentKey, StringComparison.InvariantCulture)) + { + value = _accentColor; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentDark1Key, StringComparison.InvariantCulture)) + { + value = _accentColorDark1; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentDark2Key, StringComparison.InvariantCulture)) + { + value = _accentColorDark2; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentDark3Key, StringComparison.InvariantCulture)) + { + value = _accentColorDark3; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentLight1Key, StringComparison.InvariantCulture)) + { + value = _accentColorLight1; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentLight2Key, StringComparison.InvariantCulture)) + { + value = _accentColorLight2; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentLight3Key, StringComparison.InvariantCulture)) + { + value = _accentColorLight3; + return _hasAccentColor; + } + + if (_colors.TryGetValue(strKey, out var color)) + { + value = color; + return true; + } + } + + value = null; + return false; + } + + private Color GetColor(string key) + { + if (_colors.TryGetValue(key, out var color)) + { + return color; + } + + return default; + } + + private void SetColor(string key, Color value) + { + if (value == default) + { + _colors.Remove(key); + } + else + { + _colors[key] = value; + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == AccentProperty) + { + _hasAccentColor = _accentColor != default; + + if (_hasAccentColor) + { + (_accentColorDark1, _accentColorDark2, _accentColorDark3, + _accentColorLight1, _accentColorLight2, _accentColorLight3) = + SystemAccentColors.CalculateAccentShades(_accentColor); + } + } + } +} diff --git a/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs b/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs new file mode 100644 index 0000000000..261de5497d --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs @@ -0,0 +1,65 @@ +using System; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.Themes.Fluent; + +internal class ColorPaletteResourcesCollection : AvaloniaDictionary, IResourceProvider +{ + public ColorPaletteResourcesCollection() : base(2) + { + this.ForEachItem( + (_, x) => + { + if (Owner is not null) + { + x.PropertyChanged += Palette_PropertyChanged; + } + }, + (_, x) => + { + if (Owner is not null) + { + x.PropertyChanged -= Palette_PropertyChanged; + } + }, + () => throw new NotSupportedException("Dictionary reset not supported")); + } + + public bool HasResources => Count > 0; + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) + { + theme ??= ThemeVariant.Default; + if (base.TryGetValue(theme, out var paletteResources) + && paletteResources.TryGetResource(key, theme, out value)) + { + return true; + } + + value = null; + return false; + } + + public IResourceHost? Owner { get; private set; } + public event EventHandler? OwnerChanged; + public void AddOwner(IResourceHost owner) + { + Owner = owner; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + + public void RemoveOwner(IResourceHost owner) + { + Owner = null; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + + private void Palette_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == ColorPaletteResources.AccentProperty) + { + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml index f60424a2dc..ee51ef8085 100644 --- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - + diff --git a/src/Avalonia.Themes.Fluent/Controls/Window.xaml b/src/Avalonia.Themes.Fluent/Controls/Window.xaml index ff27cce800..8db01fa4c8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Window.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Window.xaml @@ -1,7 +1,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index e83257fd9f..0528c40c21 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -1,11 +1,19 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:fluent="using:Avalonia.Themes.Fluent" + xmlns:accents="clr-namespace:Avalonia.Themes.Fluent.Accents"> - - + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs index 95539bc08a..5af22dbd1d 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Styling; @@ -31,6 +32,9 @@ namespace Avalonia.Themes.Fluent EnsureCompactStyles(); + Palettes = Resources.MergedDictionaries.OfType().FirstOrDefault() + ?? throw new InvalidOperationException("FluentTheme was initialized with missing ColorPaletteResourcesCollection."); + object GetAndRemove(string key) { var val = Resources[key] @@ -52,6 +56,8 @@ namespace Avalonia.Themes.Fluent set => SetValue(DensityStyleProperty, value); } + public IDictionary Palettes { get; } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 38af1b6d7b..0a535d2f57 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -352,7 +352,7 @@ namespace Avalonia.X11 public IEnumerable Surfaces { get; } public Action? Input { get; set; } public Action? Paint { get; set; } - public Action? Resized { get; set; } + public Action? Resized { get; set; } //TODO public Action? ScalingChanged { get; set; } public Action? Deactivated { get; set; } @@ -509,7 +509,7 @@ namespace Avalonia.X11 UpdateImePosition(); if (changedSize && !updatedSizeViaScaling && !_popup) - Resized?.Invoke(ClientSize, PlatformResizeReason.Unspecified); + Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified); }, DispatcherPriority.Layout); if (_useRenderWindow) @@ -590,7 +590,7 @@ namespace Avalonia.X11 UpdateImePosition(); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); if(!skipResize) - Resize(oldScaledSize, true, PlatformResizeReason.DpiChange); + Resize(oldScaledSize, true, WindowResizeReason.DpiChange); return true; } @@ -642,7 +642,7 @@ namespace Avalonia.X11 { // Occurs once the window has been mapped, which is the earliest the extents // can be retrieved, so invoke event to force update of TopLevel.FrameSize. - Resized?.Invoke(ClientSize, PlatformResizeReason.Unspecified); + Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified); } if (atom == _x11.Atoms._NET_WM_STATE) @@ -959,19 +959,19 @@ namespace Avalonia.X11 } - public void Resize(Size clientSize, PlatformResizeReason reason) => Resize(clientSize, false, reason); + public void Resize(Size clientSize, WindowResizeReason reason) => Resize(clientSize, false, reason); public void Move(PixelPoint point) => Position = point; private void MoveResize(PixelPoint position, Size size, double scaling) { Move(position); _scalingOverride = scaling; UpdateScaling(true); - Resize(size, true, PlatformResizeReason.Layout); + Resize(size, true, WindowResizeReason.Layout); } private PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * RenderScaling), (int)(size.Height * RenderScaling)); - private void Resize(Size clientSize, bool force, PlatformResizeReason reason) + private void Resize(Size clientSize, bool force, WindowResizeReason reason) { if (!force && clientSize == ClientSize) return; diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index d33f773bfa..8456dc92d0 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -74,7 +74,7 @@ namespace Avalonia.Browser surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height); } - Resized?.Invoke(newSize, PlatformResizeReason.User); + Resized?.Invoke(newSize, WindowResizeReason.User); (_insetsManager as BrowserInsetsManager)?.NotifySafeAreaPaddingChanged(); } @@ -241,7 +241,7 @@ namespace Avalonia.Browser public Action? SetCssCursor { get; set; } public Action? Input { get; set; } public Action? Paint { get; set; } - public Action? Resized { get; set; } + public Action? Resized { get; set; } public Action? ScalingChanged { get; set; } public Action? TransparencyLevelChanged { get; set; } public Action? Closed { get; set; } diff --git a/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj similarity index 55% rename from src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj rename to src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj index c713440dc9..1f06f28687 100644 --- a/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj +++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -2,6 +2,7 @@ net6.0;netstandard2.0 + true @@ -9,5 +10,8 @@ - + + + + diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs similarity index 69% rename from src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs rename to src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index 18c149ce2e..24703003da 100644 --- a/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -2,6 +2,7 @@ using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Platform; using Avalonia.Threading; using RemoteViewing.Vnc; using RemoteViewing.Vnc.Server; @@ -10,22 +11,28 @@ namespace Avalonia.Headless.Vnc { public class HeadlessVncFramebufferSource : IVncFramebufferSource { - public IHeadlessWindow Window { get; set; } + public Window Window { get; set; } private object _lock = new object(); public VncFramebuffer _framebuffer = new VncFramebuffer("Avalonia", 1, 1, VncPixelFormat.RGB32); private VncButton _previousButtons; public HeadlessVncFramebufferSource(VncServerSession session, Window window) { - Window = (IHeadlessWindow)window.PlatformImpl; + Window = window; session.PointerChanged += (_, args) => { var pt = new Point(args.X, args.Y); var buttons = (VncButton)args.PressedButtons; - int TranslateButton(VncButton vncButton) => - vncButton == VncButton.Left ? 0 : vncButton == VncButton.Right ? 1 : 2; + MouseButton TranslateButton(VncButton vncButton) => + vncButton switch + { + VncButton.Left => MouseButton.Left, + VncButton.Middle => MouseButton.Middle, + VncButton.Right => MouseButton.Right, + _ => MouseButton.None + }; var modifiers = (RawInputModifiers)(((int)buttons & 7) << 4); @@ -58,34 +65,25 @@ namespace Avalonia.Headless.Vnc private static VncButton[] CheckedButtons = new[] {VncButton.Left, VncButton.Middle, VncButton.Right}; - public VncFramebuffer Capture() + public unsafe VncFramebuffer Capture() { lock (_lock) { using (var bmpRef = Window.GetLastRenderedFrame()) { - if (bmpRef?.Item == null) + if (bmpRef == null) return _framebuffer; - var bmp = bmpRef.Item; + var bmp = bmpRef; if (bmp.PixelSize.Width != _framebuffer.Width || bmp.PixelSize.Height != _framebuffer.Height) { _framebuffer = new VncFramebuffer("Avalonia", bmp.PixelSize.Width, bmp.PixelSize.Height, VncPixelFormat.RGB32); } - using (var fb = bmp.Lock()) + var buffer = _framebuffer.GetBuffer(); + fixed (byte* bufferPtr = buffer) { - var buf = _framebuffer.GetBuffer(); - if (_framebuffer.Stride == fb.RowBytes) - Marshal.Copy(fb.Address, buf, 0, buf.Length); - else - for (var y = 0; y < fb.Size.Height; y++) - { - var sourceStart = fb.RowBytes * y; - var dstStart = _framebuffer.Stride * y; - var row = fb.Size.Width * 4; - Marshal.Copy(new IntPtr(sourceStart + fb.Address.ToInt64()), buf, dstStart, row); - } + bmp.CopyPixels(new PixelRect(default, bmp.PixelSize), (IntPtr)bufferPtr, buffer.Length, _framebuffer.Stride); } } } diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs similarity index 90% rename from src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs rename to src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs index efc8c66fde..8e5cd1a316 100644 --- a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Net; using System.Net.Sockets; using Avalonia.Controls; @@ -25,7 +26,7 @@ namespace Avalonia }) .AfterSetup(_ => { - var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime); + var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance!.ApplicationLifetime!); lt.Startup += async delegate { while (true) @@ -38,7 +39,7 @@ namespace Avalonia var session = new VncServerSession(); session.SetFramebufferSource(new HeadlessVncFramebufferSource( - session, lt.MainWindow)); + session, lt.MainWindow ?? throw new InvalidOperationException("MainWindow wasn't initialized"))); session.Connect(client.GetStream(), options); } diff --git a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj new file mode 100644 index 0000000000..c2c58b4f94 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj @@ -0,0 +1,19 @@ + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs new file mode 100644 index 0000000000..21086fa946 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +internal class AvaloniaTestFramework : XunitTestFramework +{ + public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink) + { + } + + protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName) + => new Executor(assemblyName, SourceInformationProvider, DiagnosticMessageSink); + + + private class Executor : XunitTestFrameworkExecutor + { + public Executor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, + IMessageSink diagnosticMessageSink) : base(assemblyName, sourceInformationProvider, + diagnosticMessageSink) + { + } + + protected override async void RunTestCases(IEnumerable testCases, + IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) + { + executionOptions.SetValue("xunit.execution.DisableParallelization", false); + using (var assemblyRunner = new AvaloniaTestRunner( + TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, + executionOptions)) await assemblyRunner.RunAsync(); + } + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs new file mode 100644 index 0000000000..3eace30805 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs @@ -0,0 +1,45 @@ +using System.Diagnostics.CodeAnalysis; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +/// +/// Sets up global avalonia test framework using avalonia application builder passed as a parameter. +/// +[TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")] +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] +public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute +{ + /// + /// Creates instance of . + /// + /// + /// Parameter from which should be created. + /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application. + /// + public AvaloniaTestFrameworkAttribute( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type appBuilderEntryPointType) { } +} + +/// +/// Discoverer implementation for the Avalonia testing framework. +/// +public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer +{ + /// + /// Creates instance of . + /// + public AvaloniaTestFrameworkTypeDiscoverer(IMessageSink _) + { + } + + /// + public Type GetTestFrameworkType(IAttributeInfo attribute) + { + var builderType = attribute.GetConstructorArguments().First() as Type + ?? throw new InvalidOperationException("AppBuilderEntryPointType parameter must be defined on the AvaloniaTestFrameworkAttribute attribute."); + return typeof(AvaloniaTestFramework<>).MakeGenericType(builderType); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs new file mode 100644 index 0000000000..42604adf46 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs @@ -0,0 +1,61 @@ +using Avalonia.Threading; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +internal class AvaloniaTestRunner : XunitTestAssemblyRunner +{ + private CancellationTokenSource? _cancellationTokenSource; + + public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable testCases, + IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, + executionMessageSink, executionOptions) + { + } + + protected override void SetupSyncContext(int maxParallelThreads) + { + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = new CancellationTokenSource(); + SynchronizationContext.SetSynchronizationContext(InitNewApplicationContext(_cancellationTokenSource.Token).Result); + } + + public override void Dispose() + { + _cancellationTokenSource?.Cancel(); + base.Dispose(); + } + + internal static Task InitNewApplicationContext(CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + + new Thread(() => + { + try + { + var appBuilder = AppBuilder.Configure(typeof(TAppBuilderEntry)); + + // If windowing subsystem wasn't initialized by user, force headless with default parameters. + if (appBuilder.WindowingSubsystemName != "Headless") + { + appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions()); + } + + appBuilder.SetupWithoutStarting(); + + tcs.SetResult(SynchronizationContext.Current!); + } + catch (Exception e) + { + tcs.SetException(e); + } + + Dispatcher.UIThread.MainLoop(cancellationToken); + }) { IsBackground = true }.Start(); + + return tcs.Task; + } +} diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj new file mode 100644 index 0000000000..b626eaeb68 --- /dev/null +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -0,0 +1,18 @@ + + + net6.0;netstandard2.0 + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs similarity index 85% rename from src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs rename to src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 90749caf6f..cefb6772c9 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -1,8 +1,7 @@ using System; using System.Diagnostics; -using Avalonia.Reactive; -using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.Reactive; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; @@ -14,11 +13,12 @@ namespace Avalonia.Headless { public static class AvaloniaHeadlessPlatform { - internal static Compositor Compositor { get; private set; } - class RenderTimer : DefaultRenderTimer + internal static Compositor? Compositor { get; private set; } + + private class RenderTimer : DefaultRenderTimer { private readonly int _framesPerSecond; - private Action _forceTick; + private Action? _forceTick; protected override IDisposable StartCore(Action tick) { bool cancelled = false; @@ -48,7 +48,7 @@ namespace Avalonia.Headless public void ForceTick() => _forceTick?.Invoke(); } - class HeadlessWindowingPlatform : IWindowingPlatform + private class HeadlessWindowingPlatform : IWindowingPlatform { public IWindowImpl CreateWindow() => new HeadlessWindowImpl(false); @@ -56,7 +56,7 @@ namespace Avalonia.Headless public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); - public ITrayIconImpl CreateTrayIcon() => null; + public ITrayIconImpl? CreateTrayIcon() => null; } internal static void Initialize(AvaloniaHeadlessPlatformOptions opts) @@ -75,7 +75,11 @@ namespace Avalonia.Headless Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), null); } - + /// + /// Forces renderer to process a rendering timer tick. + /// Use this method before calling . + /// + /// Count of frames to be ticked on the timer. public static void ForceRenderTimerTick(int count = 1) { var timer = AvaloniaLocator.Current.GetService() as RenderTimer; diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs similarity index 89% rename from src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs rename to src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index f2d86a2e9d..471dc3847d 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; using Avalonia.Media; @@ -16,12 +17,13 @@ namespace Avalonia.Headless public static void Initialize() { AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new HeadlessPlatformRenderInterface()); + .Bind().ToConstant(new HeadlessPlatformRenderInterface()) + .Bind().ToConstant(new HeadlessFontManagerStub()); } public IEnumerable InstalledFontNames { get; } = new[] { "Tahoma" }; - public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsContext) => this; public bool SupportsIndividualRoundRects => false; @@ -50,7 +52,7 @@ namespace Avalonia.Headless public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); public bool IsLost => false; - public object TryGetFeature(Type featureType) => null; + public object? TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { @@ -129,7 +131,7 @@ namespace Avalonia.Headless return new HeadlessGlyphRunStub(); } - class HeadlessGlyphRunStub : IGlyphRunImpl + private class HeadlessGlyphRunStub : IGlyphRunImpl { public Rect Bounds => new Rect(new Size(8, 12)); @@ -147,7 +149,7 @@ namespace Avalonia.Headless => Array.Empty(); } - class HeadlessGeometryStub : IGeometryImpl + private class HeadlessGeometryStub : IGeometryImpl { public HeadlessGeometryStub(Rect bounds) { @@ -160,7 +162,7 @@ namespace Avalonia.Headless public virtual bool FillContains(Point point) => Bounds.Contains(point); - public Rect GetRenderBounds(IPen pen) + public Rect GetRenderBounds(IPen? pen) { if(pen is null) { @@ -170,7 +172,7 @@ namespace Avalonia.Headless return Bounds.Inflate(pen.Thickness / 2); } - public bool StrokeContains(IPen pen, Point point) + public bool StrokeContains(IPen? pen, Point point) { return false; } @@ -194,21 +196,21 @@ namespace Avalonia.Headless return false; } - public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry) + public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, [NotNullWhen(true)] out IGeometryImpl? segmentGeometry) { segmentGeometry = null; return false; } } - class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl + private class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl { public HeadlessTransformedGeometryStub(IGeometryImpl b, Matrix transform) : this(Fix(b, transform)) { } - static (IGeometryImpl, Matrix, Rect) Fix(IGeometryImpl b, Matrix transform) + private static (IGeometryImpl, Matrix, Rect) Fix(IGeometryImpl b, Matrix transform) { if (b is HeadlessTransformedGeometryStub transformed) { @@ -230,7 +232,7 @@ namespace Avalonia.Headless public Matrix Transform { get; } } - class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl + private class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl { public HeadlessStreamingGeometryStub() : base(default) { @@ -246,7 +248,7 @@ namespace Avalonia.Headless return new HeadlessStreamingGeometryContextStub(this); } - class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl + private class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl { private readonly HeadlessStreamingGeometryStub _parent; private double _x1, _y1, _x2, _y2; @@ -255,7 +257,7 @@ namespace Avalonia.Headless _parent = parent; } - void Track(Point pt) + private void Track(Point pt) { if (_x1 > pt.X) _x1 = pt.X; @@ -304,7 +306,7 @@ namespace Avalonia.Headless } } - class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl + private class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl { public Size Size { get; } @@ -366,7 +368,7 @@ namespace Avalonia.Headless } } - class HeadlessDrawingContextStub : IDrawingContextImpl + private class HeadlessDrawingContextStub : IDrawingContextImpl { public void Dispose() { @@ -442,16 +444,16 @@ namespace Avalonia.Headless } - public object GetFeature(Type t) + public object? GetFeature(Type t) { return null; } - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { } - public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { } @@ -469,16 +471,16 @@ namespace Avalonia.Headless } - public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadow = default) + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadow = default) { } - public void DrawEllipse(IBrush brush, IPen pen, Rect rect) + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) { } - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { } @@ -489,7 +491,7 @@ namespace Avalonia.Headless } } - class HeadlessRenderTarget : IRenderTarget + private class HeadlessRenderTarget : IRenderTarget { public void Dispose() { diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs similarity index 75% rename from src/Avalonia.Headless/HeadlessPlatformStubs.cs rename to src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index aa400ab3e6..769fea7c6e 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -18,17 +18,17 @@ using Avalonia.Utilities; namespace Avalonia.Headless { - class HeadlessClipboardStub : IClipboard + internal class HeadlessClipboardStub : IClipboard { - private string _text; - private IDataObject _data; + private string? _text; + private IDataObject? _data; - public Task GetTextAsync() + public Task GetTextAsync() { return Task.Run(() => _text); } - public Task SetTextAsync(string text) + public Task SetTextAsync(string? text) { return Task.Run(() => _text = text); } @@ -45,16 +45,29 @@ namespace Avalonia.Headless public Task GetFormatsAsync() { - throw new NotImplementedException(); + return Task.Run(() => + { + if (_data is not null) + { + return _data.GetDataFormats().ToArray(); + } + + if (_text is not null) + { + return new[] { DataFormats.Text }; + } + + return Array.Empty(); + }); } - public async Task GetDataAsync(string format) + public async Task GetDataAsync(string format) { return await Task.Run(() => _data); } } - class HeadlessCursorFactoryStub : ICursorFactory + internal class HeadlessCursorFactoryStub : ICursorFactory { public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); @@ -65,7 +78,7 @@ namespace Avalonia.Headless } } - class HeadlessGlyphTypefaceImpl : IGlyphTypeface + internal class HeadlessGlyphTypefaceImpl : IGlyphTypeface { public FontMetrics Metrics => new FontMetrics { @@ -125,7 +138,7 @@ namespace Avalonia.Headless public bool TryGetTable(uint tag, out byte[] table) { - table = null; + table = null!; return false; } @@ -141,7 +154,7 @@ namespace Avalonia.Headless } } - class HeadlessTextShaperStub : ITextShaperImpl + internal class HeadlessTextShaperStub : ITextShaperImpl { public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { @@ -153,7 +166,7 @@ namespace Avalonia.Headless } } - class HeadlessFontManagerStub : IFontManagerImpl + internal class HeadlessFontManagerStub : IFontManagerImpl { public string GetDefaultFontFamilyName() { @@ -179,17 +192,16 @@ namespace Avalonia.Headless return true; } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo culture, out Typeface typeface) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface) { typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch); return true; } } - class HeadlessIconLoaderStub : IPlatformIconLoader + internal class HeadlessIconLoaderStub : IPlatformIconLoader { - - class IconStub : IWindowIconImpl + private class IconStub : IWindowIconImpl { public void Save(Stream outputStream) { @@ -212,7 +224,7 @@ namespace Avalonia.Headless } } - class HeadlessScreensStub : IScreenImpl + internal class HeadlessScreensStub : IScreenImpl { public int ScreenCount { get; } = 1; @@ -222,40 +234,19 @@ namespace Avalonia.Headless new PixelRect(0, 0, 1920, 1280), true), }; - public Screen ScreenFromPoint(PixelPoint point) + public Screen? ScreenFromPoint(PixelPoint point) { return ScreenHelper.ScreenFromPoint(point, AllScreens); } - public Screen ScreenFromRect(PixelRect rect) + public Screen? ScreenFromRect(PixelRect rect) { return ScreenHelper.ScreenFromRect(rect, AllScreens); } - public Screen ScreenFromWindow(IWindowBaseImpl window) + public Screen? ScreenFromWindow(IWindowBaseImpl window) { return ScreenHelper.ScreenFromWindow(window, AllScreens); } } - - internal class NoopStorageProvider : BclStorageProvider - { - public override bool CanOpen => false; - public override Task> OpenFilePickerAsync(FilePickerOpenOptions options) - { - return Task.FromResult>(Array.Empty()); - } - - public override bool CanSave => false; - public override Task SaveFilePickerAsync(FilePickerSaveOptions options) - { - return Task.FromResult(null); - } - - public override bool CanPickFolder => false; - public override Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) - { - return Task.FromResult>(Array.Empty()); - } - } } diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs new file mode 100644 index 0000000000..8fbc5ec6ef --- /dev/null +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -0,0 +1,101 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Threading; + +namespace Avalonia.Headless; + +/// +/// Set of extension methods to simplify usage of Avalonia.Headless platform. +/// +public static class HeadlessWindowExtensions +{ + /// + /// Triggers a renderer timer tick and captures last rendered frame. + /// + /// Bitmap with last rendered frame. Null, if nothing was rendered. + public static Bitmap? CaptureRenderedFrame(this TopLevel topLevel) + { + Dispatcher.UIThread.RunJobs(); + AvaloniaHeadlessPlatform.ForceRenderTimerTick(); + return topLevel.GetLastRenderedFrame(); + } + + /// + /// Reads last rendered frame. + /// Note, in order to trigger rendering timer, call method. + /// + /// Bitmap with last rendered frame. Null, if nothing was rendered. + public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel) + { + if (AvaloniaLocator.Current.GetService() is HeadlessPlatformRenderInterface) + { + throw new NotSupportedException( + "To capture a rendered frame, make sure that headless application was initialized with '.UseSkia()' and disabled 'UseHeadlessDrawing' in the 'AvaloniaHeadlessPlatformOptions'."); + } + + return GetImpl(topLevel).GetLastRenderedFrame(); + } + + /// + /// Simulates keyboard press on the headless window/toplevel. + /// + public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => + RunJobsAndGetImpl(topLevel).KeyPress(key, modifiers); + + /// + /// Simulates keyboard release on the headless window/toplevel. + /// + public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => + RunJobsAndGetImpl(topLevel).KeyRelease(key, modifiers); + + /// + /// Simulates mouse down on the headless window/toplevel. + /// + public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseDown(point, button, modifiers); + + /// + /// Simulates mouse move on the headless window/toplevel. + /// + public static void MouseMove(this TopLevel topLevel, Point point, + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseMove(point, modifiers); + + /// + /// Simulates mouse up on the headless window/toplevel. + /// + public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseUp(point, button, modifiers); + + /// + /// Simulates mouse wheel on the headless window/toplevel. + /// + public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseWheel(point, delta, modifiers); + + /// + /// Simulates drag'n'drop target on the headless window/toplevel. + /// + public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, + DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); + + private static IHeadlessWindow RunJobsAndGetImpl(this TopLevel topLevel) + { + Dispatcher.UIThread.RunJobs(); + return GetImpl(topLevel); + } + + private static IHeadlessWindow GetImpl(this TopLevel topLevel) + { + return topLevel.PlatformImpl as IHeadlessWindow ?? + throw new InvalidOperationException("TopLevel must be a headless window."); + } +} diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs similarity index 68% rename from src/Avalonia.Headless/HeadlessWindowImpl.cs rename to src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index a801474f21..b15c1eb327 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -17,20 +17,20 @@ using Avalonia.Utilities; namespace Avalonia.Headless { - class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow + internal class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow { - private IKeyboardDevice _keyboard; - private Stopwatch _st = Stopwatch.StartNew(); - private Pointer _mousePointer; - private WriteableBitmap _lastRenderedFrame; - private object _sync = new object(); + private readonly IKeyboardDevice _keyboard; + private readonly Stopwatch _st = Stopwatch.StartNew(); + private readonly Pointer _mousePointer; + private WriteableBitmap? _lastRenderedFrame; + private readonly object _sync = new object(); public bool IsPopup { get; } public HeadlessWindowImpl(bool isPopup) { IsPopup = isPopup; Surfaces = new object[] { this }; - _keyboard = AvaloniaLocator.Current.GetService(); + _keyboard = AvaloniaLocator.Current.GetRequiredService(); _mousePointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); MouseDevice = new MouseDevice(_mousePointer); ClientSize = new Size(1024, 768); @@ -48,13 +48,13 @@ namespace Avalonia.Headless public double RenderScaling { get; } = 1; public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } - public Action Input { get; set; } - public Action Paint { get; set; } - public Action Resized { get; set; } - public Action ScalingChanged { get; set; } + public Action? Input { get; set; } + public Action? Paint { get; set; } + public Action? Resized { get; set; } + public Action? ScalingChanged { get; set; } public IRenderer CreateRenderer(IRenderRoot root) => - new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor, () => Surfaces); + new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor!, () => Surfaces); public void Invalidate(Rect rect) { @@ -65,18 +65,18 @@ namespace Avalonia.Headless InputRoot = inputRoot; } - public IInputRoot InputRoot { get; set; } + public IInputRoot? InputRoot { get; set; } public Point PointToClient(PixelPoint point) => point.ToPoint(RenderScaling); public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); - public void SetCursor(ICursorImpl cursor) + public void SetCursor(ICursorImpl? cursor) { } - public Action Closed { get; set; } + public Action? Closed { get; set; } public IMouseDevice MouseDevice { get; } public void Show(bool activate, bool isDialog) @@ -101,17 +101,17 @@ namespace Avalonia.Headless } public PixelPoint Position { get; set; } - public Action PositionChanged { get; set; } + public Action? PositionChanged { get; set; } public void Activate() { Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); } - public Action Deactivated { get; set; } - public Action Activated { get; set; } + public Action? Deactivated { get; set; } + public Action? Activated { get; set; } public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB"); public Size MaxClientSize { get; } = new Size(1920, 1280); - public void Resize(Size clientSize, PlatformResizeReason reason) + public void Resize(Size clientSize, WindowResizeReason reason) { // Emulate X11 behavior here if (IsPopup) @@ -123,13 +123,13 @@ namespace Avalonia.Headless }); } - void DoResize(Size clientSize) + private void DoResize(Size clientSize) { // Uncomment this check and experience a weird bug in layout engine if (ClientSize != clientSize) { ClientSize = clientSize; - Resized?.Invoke(clientSize, PlatformResizeReason.Unspecified); + Resized?.Invoke(clientSize, WindowResizeReason.Unspecified); } } @@ -145,8 +145,8 @@ namespace Avalonia.Headless public IScreenImpl Screen { get; } = new HeadlessScreensStub(); public WindowState WindowState { get; set; } - public Action WindowStateChanged { get; set; } - public void SetTitle(string title) + public Action? WindowStateChanged { get; set; } + public void SetTitle(string? title) { } @@ -156,7 +156,7 @@ namespace Avalonia.Headless } - public void SetIcon(IWindowIconImpl icon) + public void SetIcon(IWindowIconImpl? icon) { } @@ -171,9 +171,9 @@ namespace Avalonia.Headless } - public Func Closing { get; set; } + public Func? Closing { get; set; } - class FramebufferProxy : ILockedFramebuffer + private class FramebufferProxy : ILockedFramebuffer { private readonly ILockedFramebuffer _fb; private readonly Action _onDispose; @@ -214,28 +214,37 @@ namespace Avalonia.Headless }); } - public IRef GetLastRenderedFrame() + public Bitmap? GetLastRenderedFrame() { lock (_sync) - return _lastRenderedFrame?.PlatformImpl?.CloneAs(); + { + if (_lastRenderedFrame is null) + { + return null; + } + + using var lockedFramebuffer = _lastRenderedFrame.Lock(); + return new Bitmap(lockedFramebuffer.Format, AlphaFormat.Opaque, lockedFramebuffer.Address, + lockedFramebuffer.Size, lockedFramebuffer.Dpi, lockedFramebuffer.RowBytes); + } } private ulong Timestamp => (ulong)_st.ElapsedMilliseconds; // TODO: Hook recent Popup changes. - IPopupPositioner IPopupImpl.PopupPositioner => null; + IPopupPositioner IPopupImpl.PopupPositioner => null!; public Size MaxAutoSizeHint => new Size(1920, 1080); - public Action TransparencyLevelChanged { get; set; } + public Action? TransparencyLevelChanged { get; set; } public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; - public Action GotInputWhenDisabled { get; set; } + public Action? GotInputWhenDisabled { get; set; } public bool IsClientAreaExtendedToDecorations => false; - public Action ExtendClientAreaToDecorationsChanged { get; set; } + public Action? ExtendClientAreaToDecorationsChanged { get; set; } public bool NeedsManagedDecorations => false; @@ -243,17 +252,12 @@ namespace Avalonia.Headless public Thickness OffScreenMargin => new Thickness(); - public Action LostFocus { get; set; } + public Action? LostFocus { get; set; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); - public object TryGetFeature(Type featureType) + public object? TryGetFeature(Type featureType) { - if (featureType == typeof(IStorageProvider)) - { - return new NoopStorageProvider(); - } - - if(featureType == typeof(IClipboard)) + if(featureType == typeof(IClipboard)) { return AvaloniaLocator.Current.GetRequiredService(); } @@ -263,46 +267,58 @@ namespace Avalonia.Headless void IHeadlessWindow.KeyPress(Key key, RawInputModifiers modifiers) { - Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyDown, key, modifiers)); + Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyDown, key, modifiers)); } void IHeadlessWindow.KeyRelease(Key key, RawInputModifiers modifiers) { - Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyUp, key, modifiers)); + Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyUp, key, modifiers)); } - void IHeadlessWindow.MouseDown(Point point, int button, RawInputModifiers modifiers) + void IHeadlessWindow.MouseDown(Point point, MouseButton button, RawInputModifiers modifiers) { - Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, - button == 0 ? RawPointerEventType.LeftButtonDown : - button == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.RightButtonDown, - point, modifiers)); + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, + button switch + { + MouseButton.Left => RawPointerEventType.LeftButtonDown, + MouseButton.Right => RawPointerEventType.RightButtonDown, + MouseButton.Middle => RawPointerEventType.MiddleButtonDown, + MouseButton.XButton1 => RawPointerEventType.XButton1Down, + MouseButton.XButton2 => RawPointerEventType.XButton2Down, + _ => RawPointerEventType.Move, + }, point, modifiers)); } void IHeadlessWindow.MouseMove(Point point, RawInputModifiers modifiers) { - Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, RawPointerEventType.Move, point, modifiers)); } - void IHeadlessWindow.MouseUp(Point point, int button, RawInputModifiers modifiers) + void IHeadlessWindow.MouseUp(Point point, MouseButton button, RawInputModifiers modifiers) { - Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, - button == 0 ? RawPointerEventType.LeftButtonUp : - button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp, - point, modifiers)); + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, + button switch + { + MouseButton.Left => RawPointerEventType.LeftButtonUp, + MouseButton.Right => RawPointerEventType.RightButtonUp, + MouseButton.Middle => RawPointerEventType.MiddleButtonUp, + MouseButton.XButton1 => RawPointerEventType.XButton1Up, + MouseButton.XButton2 => RawPointerEventType.XButton2Up, + _ => RawPointerEventType.Move, + }, point, modifiers)); } void IHeadlessWindow.MouseWheel(Point point, Vector delta, RawInputModifiers modifiers) { - Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, InputRoot!, point, delta, modifiers)); } void IHeadlessWindow.DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) { var device = AvaloniaLocator.Current.GetRequiredService(); - Input?.Invoke(new RawDragEvent(device, type, InputRoot, point, data, effects, modifiers)); + Input?.Invoke(new RawDragEvent(device, type, InputRoot!, point, data, effects, modifiers)); } void IWindowImpl.Move(PixelPoint point) @@ -310,7 +326,7 @@ namespace Avalonia.Headless } - public IPopupImpl CreatePopup() + public IPopupImpl? CreatePopup() { // TODO: Hook recent Popup changes. return null; diff --git a/src/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs similarity index 65% rename from src/Avalonia.Headless/IHeadlessWindow.cs rename to src/Headless/Avalonia.Headless/IHeadlessWindow.cs index dfb3a4c433..f3da2335bc 100644 --- a/src/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs @@ -6,15 +6,15 @@ using Avalonia.Utilities; namespace Avalonia.Headless { - public interface IHeadlessWindow + internal interface IHeadlessWindow { - IRef GetLastRenderedFrame(); + Bitmap? GetLastRenderedFrame(); void KeyPress(Key key, RawInputModifiers modifiers); void KeyRelease(Key key, RawInputModifiers modifiers); - void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); + void MouseDown(Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None); void MouseMove(Point point, RawInputModifiers modifiers = RawInputModifiers.None); - void MouseUp(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); + void MouseUp(Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None); void MouseWheel(Point point, Vector delta, RawInputModifiers modifiers = RawInputModifiers.None); - void DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers); + void DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index af4a70f128..ccc8cab8ae 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -65,7 +65,7 @@ using Avalonia.Rendering.Composition; public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlArrayConstantAstNode.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlArrayConstantAstNode.cs new file mode 100644 index 0000000000..339b720d10 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlArrayConstantAstNode.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Reflection.Emit; +using Avalonia.Controls; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.AstNodes +{ + class AvaloniaXamlIlArrayConstantAstNode : XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode + { + private readonly IXamlType _elementType; + private readonly IReadOnlyList _values; + + public AvaloniaXamlIlArrayConstantAstNode(IXamlLineInfo lineInfo, IXamlType arrayType, IXamlType elementType, IReadOnlyList values) : base(lineInfo) + { + _elementType = elementType; + _values = values; + + Type = new XamlAstClrTypeReference(lineInfo, arrayType, false); + + foreach (var element in values) + { + if (!elementType.IsAssignableFrom(element.Type.GetClrType())) + { + throw new XamlParseException("x:Array element is not assignable to the array element type!", lineInfo); + } + } + } + + public IXamlAstTypeReference Type { get; } + + public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldc_I4(_values.Count) + .Newarr(_elementType); + + for (var index = 0; index < _values.Count; index++) + { + var value = _values[index]; + + codeGen + .Dup() + .Ldc_I4(index); + + context.Emit(value, codeGen, _elementType); + + if (value.Type.GetClrType() is { IsValueType: true } valTypeInObjArr) + { + if (!_elementType.IsValueType) + { + codeGen.Box(valTypeInObjArr); + } + // It seems like ASM codegen for "stelem valuetype" and "stelem.i4" is identical, + // so we don't need to try to optimize it here. + codeGen.Emit(OpCodes.Stelem, valTypeInObjArr); + } + else + { + codeGen.Stelem_ref(); + } + } + + return XamlILNodeEmitResult.Type(0, Type.GetClrType()); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 5ca2b09eba..23c67df810 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -58,7 +58,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), - new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() + new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer(), + new AvaloniaXamlIlThemeVariantProviderTransformer() ); InsertBefore( new AvaloniaXamlIlOptionMarkupExtensionTransformer()); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index d8524cfd88..cd005ce24d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -198,6 +198,29 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a grid length", node); } } + + if (type.Equals(types.ColumnDefinition) || type.Equals(types.RowDefinition)) + { + try + { + var gridLength = GridLength.Parse(text); + + result = new AvaloniaXamlIlGridLengthAstNode(node, types, gridLength); + + var definitionConstructorGridLength = type.GetConstructor(new List {types.GridLength}); + var lengthNode = new AvaloniaXamlIlGridLengthAstNode(node, types, gridLength); + var definitionTypeRef = new XamlAstClrTypeReference(node, type, false); + + result = new XamlAstNewClrObjectNode(node, definitionTypeRef, + definitionConstructorGridLength, new List {lengthNode}); + + return true; + } + catch + { + throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a grid length", node); + } + } if (type.Equals(types.Cursor)) { @@ -211,16 +234,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } - if (type.Equals(types.ColumnDefinitions)) - { - return ConvertDefinitionList(node, text, types, types.ColumnDefinitions, types.ColumnDefinition, "column definitions", out result); - } - - if (type.Equals(types.RowDefinitions)) - { - return ConvertDefinitionList(node, text, types, types.RowDefinitions, types.RowDefinition, "row definitions", out result); - } - if (types.IBrush.IsAssignableFrom(type)) { if (Color.TryParse(text, out Color color)) @@ -295,46 +308,89 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } - result = null; - return false; - } - - private static bool ConvertDefinitionList( - IXamlAstValueNode node, - string text, - AvaloniaXamlIlWellKnownTypes types, - IXamlType listType, - IXamlType elementType, - string errorDisplayName, - out IXamlAstValueNode result) - { - try + // Keep it in the end, so more specific parsers can be applied. + var elementType = GetElementType(type, context.Configuration.WellKnownTypes); + if (elementType is not null) { - var lengths = GridLength.ParseLengths(text); - - var definitionTypeRef = new XamlAstClrTypeReference(node, elementType, false); + string[] items; + // Normalize special case of Points collection. + if (elementType == types.Point) + { + var pointParts = text.Split(new[] { ",", " " }, StringSplitOptions.RemoveEmptyEntries); + if (pointParts.Length % 2 == 0) + { + items = new string[pointParts.Length / 2]; + for (int i = 0; i < pointParts.Length; i += 2) + { + items[i / 2] = string.Format(CultureInfo.InvariantCulture, "{0} {1}", pointParts[i], + pointParts[i + 1]); + } + } + else + { + throw new XamlX.XamlLoadException($"Invalid PointsList.", node); + } + } + else + { + const StringSplitOptions trimOption = (StringSplitOptions)2; // StringSplitOptions.TrimEntries + var separators = new[] { "," }; + var splitOptions = StringSplitOptions.RemoveEmptyEntries | trimOption; - var definitionConstructorGridLength = elementType.GetConstructor(new List {types.GridLength}); + items = text.Split(separators, splitOptions ^ trimOption); + // Compiler targets netstandard, so we need to emulate StringSplitOptions.TrimEntries, if it was requested. + if (splitOptions.HasFlag(trimOption)) + { + items = items.Select(i => i.Trim()).ToArray(); + } + } - IXamlAstValueNode CreateDefinitionNode(GridLength length) + var nodes = new IXamlAstValueNode[items.Length]; + for (var index = 0; index < items.Length; index++) { - var lengthNode = new AvaloniaXamlIlGridLengthAstNode(node, types, length); + var success = XamlTransformHelpers.TryGetCorrectlyTypedValue( + context, + new XamlAstTextNode(node, items[index], true, context.Configuration.WellKnownTypes.String), + elementType, out var itemNode); + if (!success) + { + result = null; + return false; + } - return new XamlAstNewClrObjectNode(node, definitionTypeRef, - definitionConstructorGridLength, new List {lengthNode}); + nodes[index] = itemNode; } - var definitionNodes = - new List(lengths.Select(CreateDefinitionNode)); - - result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, listType, elementType, definitionNodes); + if (types.AvaloniaList.MakeGenericType(elementType).IsAssignableFrom(type)) + { + result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, type, elementType, nodes); + return true; + } + else if (type.IsArray) + { + result = new AvaloniaXamlIlArrayConstantAstNode(node, elementType.MakeArrayType(1), elementType, nodes); + return true; + } + else if (type == context.Configuration.WellKnownTypes.IListOfT.MakeGenericType(elementType)) + { + var listType = context.Configuration.WellKnownTypes.IListOfT.MakeGenericType(elementType); + result = new AvaloniaXamlIlArrayConstantAstNode(node, listType, elementType, nodes); + return true; + } - return true; - } - catch - { - throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a {errorDisplayName}", node); + result = null; + return false; } + + result = null; + return false; + } + + private static IXamlType GetElementType(IXamlType type, XamlTypeWellKnownTypes types) + { + return type.GetAllInterfaces().FirstOrDefault(i => + i.FullName.StartsWith(types.IEnumerableT.FullName))? + .GenericArguments[0]; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs index db8d604154..8e04a7d467 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs @@ -24,7 +24,7 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer var mergeResourceIncludeType = context.GetAvaloniaTypes().MergeResourceInclude; var mergeSourceNodes = new List(); - var hasAnyNonMergedResource = false; + var mergedResourceWasAdded = false; foreach (var manipulationNode in resourceDictionaryManipulation.Children.ToArray()) { void ProcessXamlPropertyAssignmentNode(XamlManipulationGroupNode parent, XamlPropertyAssignmentNode assignmentNode) @@ -38,7 +38,8 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer && objectInitialization.Manipulation is XamlPropertyAssignmentNode sourceAssignmentNode) { parent.Children.Remove(assignmentNode); - mergeSourceNodes.Add(sourceAssignmentNode); + mergeSourceNodes.Add(sourceAssignmentNode); + mergedResourceWasAdded = true; } else { @@ -47,15 +48,10 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer valueNode); } } - else - { - hasAnyNonMergedResource = true; - } - - if (hasAnyNonMergedResource && mergeSourceNodes.Any()) + else if (mergeSourceNodes.Any()) { throw new XamlDocumentParseException(context.CurrentDocument, - "Mix of MergeResourceInclude and other dictionaries inside of the ResourceDictionary.MergedDictionaries is not allowed", + "MergeResourceInclude should always be included last when mixing with other dictionaries inside of the ResourceDictionary.MergedDictionaries.", valueNode); } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs new file mode 100644 index 0000000000..05df8be1b6 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs @@ -0,0 +1,31 @@ +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; + +internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransformer +{ + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + var type = context.GetAvaloniaTypes().IThemeVariantProvider; + if (!(node is XamlAstObjectNode on + && type.IsAssignableFrom(on.Type.GetClrType()))) + return node; + + var keyDirective = on.Children.FirstOrDefault(n => n is XamlAstXmlDirective d + && d.Namespace == XamlNamespaces.Xaml2006 && + d.Name == "Key") as XamlAstXmlDirective; + if (keyDirective is null) + return node; + + var keyProp = type.Properties.First(p => p.Name == "Key"); + on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective, + new XamlAstClrProperty(keyDirective, keyProp, context.Configuration), + keyDirective.Values, true)); + + return node; + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 60a7d953ab..8ab84f4615 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -33,6 +33,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType InheritDataTypeFromItemsAttribute { get; } public IXamlType MarkupExtensionOptionAttribute { get; } public IXamlType MarkupExtensionDefaultOptionAttribute { get; } + public IXamlType AvaloniaList { get; } public IXamlType OnExtensionType { get; } public IXamlType UnsetValueType { get; } public IXamlType StyledElement { get; } @@ -109,6 +110,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType IResourceDictionary { get; } public IXamlType ResourceDictionary { get; } public IXamlMethod ResourceDictionaryDeferredAdd { get; } + public IXamlType IThemeVariantProvider { get; } public IXamlType UriKind { get; } public IXamlConstructor UriConstructor { get; } public IXamlType Style { get; } @@ -141,6 +143,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers InheritDataTypeFromItemsAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromItemsAttribute"); MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute"); MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute"); + AvaloniaList = cfg.TypeSystem.GetType("Avalonia.Collections.AvaloniaList`1"); OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On"); AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, AvaloniaObject, AvaloniaProperty, @@ -248,6 +251,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers cfg.TypeSystem.GetType("System.Func`2").MakeGenericType( cfg.TypeSystem.GetType("System.IServiceProvider"), XamlIlTypes.Object)); + IThemeVariantProvider = cfg.TypeSystem.GetType("Avalonia.Controls.IThemeVariantProvider"); UriKind = cfg.TypeSystem.GetType("System.UriKind"); UriConstructor = Uri.GetConstructor(new List() { cfg.WellKnownTypes.String, UriKind }); Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style"); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index 5dd0b042e1..5d1025f30d 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit 5dd0b042e144e677638224c49fec16dab66143e8 +Subproject commit 5d1025f30d0ed6d8f419d82959c148276301f393 diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index e1b594e331..7f52c872ed 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Data; using Avalonia.Markup.Xaml.Converters; using Avalonia.Media; +using Avalonia.Styling; namespace Avalonia.Markup.Xaml.MarkupExtensions { @@ -10,6 +11,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { private object? _anchor; private BindingPriority _priority; + private ThemeVariant? _currentThemeVariant; public DynamicResourceExtension() { @@ -36,6 +38,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions (object?)serviceProvider.GetFirstParent(); } + _currentThemeVariant = StaticResourceExtension.GetDictionaryVariant(serviceProvider); + return this; } @@ -59,7 +63,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions } else if (_anchor is IResourceProvider resourceProvider) { - var source = resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); + var source = resourceProvider.GetResourceObservable(ResourceKey, _currentThemeVariant, GetConverter(targetProperty)); return InstancedBinding.OneWay(source, _priority); } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 3de669b1e4..c23c31e24c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -33,7 +33,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions var provideTarget = serviceProvider.GetService(); var targetObject = provideTarget?.TargetObject; var targetProperty = provideTarget?.TargetProperty; - var themeVariant = (targetObject as IThemeVariantHost)?.ActualThemeVariant; + var themeVariant = (targetObject as IThemeVariantHost)?.ActualThemeVariant + ?? GetDictionaryVariant(serviceProvider); var targetType = targetProperty switch { @@ -78,6 +79,25 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { return ColorToBrushConverter.Convert(control.FindResource(ResourceKey!), targetType); } + + internal static ThemeVariant? GetDictionaryVariant(IServiceProvider serviceProvider) + { + var parents = serviceProvider.GetService()?.Parents; + if (parents is null) + { + return null; + } + + foreach (var parent in parents) + { + if (parent is IThemeVariantProvider { Key: { } setKey }) + { + return setKey; + } + } + + return null; + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs index fbcfdde565..eee02ea0d8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs @@ -13,7 +13,7 @@ namespace Avalonia.Markup.Xaml.Styling /// When used in runtime, this type might be unsafe with trimming and AOT. /// [RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] - public class ResourceInclude : IResourceProvider + public class ResourceInclude : IResourceProvider, IThemeVariantProvider { private readonly IServiceProvider? _serviceProvider; private readonly Uri? _baseUri; @@ -65,6 +65,8 @@ namespace Avalonia.Markup.Xaml.Styling /// public Uri? Source { get; set; } + ThemeVariant? IThemeVariantProvider.Key { get; set; } + bool IResourceNode.HasResources => Loaded.HasResources; public event EventHandler? OwnerChanged diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs index 4bbdda31d8..b94eccf7c0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Metadata; @@ -13,6 +14,6 @@ namespace Avalonia.Markup.Xaml.Templates public Type? TargetType { get; set; } - public ControlTemplateResult? Build(TemplatedControl control) => TemplateContent.Load(Content); + public TemplateResult? Build(TemplatedControl control) => TemplateContent.Load(Content); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 89b0468c6e..b45898d8bd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -30,7 +30,7 @@ namespace Avalonia.Markup.Xaml.Templates public Control? Build(object? data, Control? existing) { - return existing ?? TemplateContent.Load(Content)?.Control; + return existing ?? TemplateContent.Load(Content)?.Result; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs index c228a58990..f31a693e72 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object? Content { get; set; } - public Panel? Build() => (Panel?)TemplateContent.Load(Content)?.Control; + public Panel? Build() => (Panel?)TemplateContent.Load(Content)?.Result; object? ITemplate.Build() => Build(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs index 62febebc8c..5999a8021e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object? Content { get; set; } - public Control? Build() => TemplateContent.Load(Content)?.Control; + public Control? Build() => TemplateContent.Load(Content)?.Result; object? ITemplate.Build() => Build(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 08e897c514..504478f9b3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -1,15 +1,16 @@ using System; +using Avalonia.Controls; using Avalonia.Controls.Templates; namespace Avalonia.Markup.Xaml.Templates { public static class TemplateContent { - public static ControlTemplateResult? Load(object? templateContent) + public static TemplateResult? Load(object? templateContent) { if (templateContent is Func direct) { - return (ControlTemplateResult?)direct(null); + return (TemplateResult?)direct(null); } if (templateContent is null) diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index a5b308523f..98c3b61c9f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -54,7 +54,7 @@ namespace Avalonia.Markup.Xaml.Templates public Control? Build(object? data) { - var visualTreeForItem = TemplateContent.Load(Content)?.Control; + var visualTreeForItem = TemplateContent.Load(Content)?.Result; if (visualTreeForItem != null) { visualTreeForItem.DataContext = data; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index ba96ac15b3..0cc7cc5468 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -35,7 +35,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime scope.Complete(); if(typeof(T) == typeof(Control)) - return new ControlTemplateResult((Control)obj, scope); + return new TemplateResult((Control)obj, scope); return new TemplateResult((T)obj, scope); }; diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 13eae1992c..24fd7e3933 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -108,7 +108,7 @@ namespace Avalonia.Win32.Interop.Wpf if (_finalSize == _previousSize) return finalSize; _previousSize = _finalSize; - _ttl.Resized?.Invoke(finalSize.ToAvaloniaSize(), PlatformResizeReason.Unspecified); + _ttl.Resized?.Invoke(finalSize.ToAvaloniaSize(), WindowResizeReason.Unspecified); return base.ArrangeOverride(finalSize); } @@ -229,7 +229,7 @@ namespace Avalonia.Win32.Interop.Wpf Action ITopLevelImpl.Input { get; set; } //TODO Action ITopLevelImpl.Paint { get; set; } - Action ITopLevelImpl.Resized { get; set; } + Action ITopLevelImpl.Resized { get; set; } Action ITopLevelImpl.ScalingChanged { get; set; } Action ITopLevelImpl.TransparencyLevelChanged { get; set; } diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index db650db4b0..aabf361844 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -85,16 +85,18 @@ namespace Avalonia.Win32.Input _parent = parent; - var langId= PRIMARYLANGID(LGID(HKL)); + var langId = PRIMARYLANGID(LGID(HKL)); - if(langId != _langId) + if (IsActive) { - DisableImm(); + if (langId != _langId) + { + DisableImm(); + EnableImm(); + } } _langId = langId; - - EnableImm(); } public void ClearLanguageAndWindow() diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 75c1a2d564..1470435134 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Platform; using Avalonia.Win32.Interop; @@ -135,7 +136,7 @@ namespace Avalonia.Win32 private void MoveResize(PixelPoint position, Size size, double scaling) { Move(position); - Resize(size, PlatformResizeReason.Layout); + Resize(size, WindowResizeReason.Layout); //TODO: We ignore the scaling override for now } diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 05d9faa97b..8f9fc5fa80 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -212,7 +212,7 @@ namespace Avalonia.Win32 if (PlatformImpl is { } platformImpl) { platformImpl.Move(position); - platformImpl.Resize(size, PlatformResizeReason.Layout); + platformImpl.Resize(size, WindowResizeReason.Layout); } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 0cb8b09579..2a3255bb70 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -133,7 +133,7 @@ namespace Avalonia.Win32 _scaling = dpi / 96.0; ScalingChanged?.Invoke(_scaling); - using (SetResizeReason(PlatformResizeReason.DpiChange)) + using (SetResizeReason(WindowResizeReason.DpiChange)) { SetWindowPos(hWnd, IntPtr.Zero, @@ -611,7 +611,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_ENTERSIZEMOVE: - _resizeReason = PlatformResizeReason.User; + _resizeReason = WindowResizeReason.User; break; case WindowsMessage.WM_SIZE: @@ -658,7 +658,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_EXITSIZEMOVE: - _resizeReason = PlatformResizeReason.Unspecified; + _resizeReason = WindowResizeReason.Unspecified; break; case WindowsMessage.WM_MOVE: diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 545513c732..9217f42952 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -59,7 +59,7 @@ namespace Avalonia.Win32 private double _extendTitleBarHint = -1; private readonly bool _isUsingComposition; private readonly IBlurHost? _blurHost; - private PlatformResizeReason _resizeReason; + private WindowResizeReason _resizeReason; private MOUSEMOVEPOINT _lastWmMousePoint; #if USE_MANAGED_DRAG @@ -200,7 +200,7 @@ namespace Avalonia.Win32 public Action? Paint { get; set; } - public Action? Resized { get; set; } + public Action? Resized { get; set; } public Action? ScalingChanged { get; set; } @@ -588,7 +588,7 @@ namespace Avalonia.Win32 public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, Win32Platform.Compositor, () => Surfaces); - public void Resize(Size value, PlatformResizeReason reason) + public void Resize(Size value, WindowResizeReason reason) { if (WindowState != WindowState.Normal) return; @@ -1053,7 +1053,7 @@ namespace Avalonia.Win32 _offScreenMargin = new Thickness(); _extendedMargins = new Thickness(); - Resize(new Size(rcWindow.Width / RenderScaling, rcWindow.Height / RenderScaling), PlatformResizeReason.Layout); + Resize(new Size(rcWindow.Width / RenderScaling, rcWindow.Height / RenderScaling), WindowResizeReason.Layout); unsafe { @@ -1462,7 +1462,7 @@ namespace Avalonia.Win32 /// public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); - private ResizeReasonScope SetResizeReason(PlatformResizeReason reason) + private ResizeReasonScope SetResizeReason(WindowResizeReason reason) { var old = _resizeReason; _resizeReason = reason; @@ -1487,9 +1487,9 @@ namespace Avalonia.Win32 private struct ResizeReasonScope : IDisposable { private readonly WindowImpl _owner; - private readonly PlatformResizeReason _restore; + private readonly WindowResizeReason _restore; - public ResizeReasonScope(WindowImpl owner, PlatformResizeReason restore) + public ResizeReasonScope(WindowImpl owner, WindowResizeReason restore) { _owner = owner; _restore = restore; diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index ec6ea56d9d..6ca0cf7ace 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -150,7 +150,7 @@ namespace Avalonia.iOS public IEnumerable Surfaces { get; set; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } public Action Closed { get; set; } @@ -225,7 +225,7 @@ namespace Avalonia.iOS public override void LayoutSubviews() { - _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize, PlatformResizeReason.Layout); + _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize, WindowResizeReason.Layout); base.LayoutSubviews(); } diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs index 98d2807db5..9f13520086 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs @@ -457,5 +457,39 @@ namespace Avalonia.Base.UnitTests.Layout Assert.Equal(1, layoutCount); } + + [Fact] + public void Child_Can_Invalidate_Parent_Measure_During_Arrange() + { + // Issue #11015. + // + // - Child invalidates parent measure in arrange pass + // - Parent is added to measure & arrange queues + // - Arrange pass dequeues parent + // - Measure is not valid so parent is not arranged + // - Parent is measured + // - Parent has been dequeued from arrange queue so no arrange is performed + var child = new LayoutTestControl(); + var parent = new LayoutTestControl { Child = child }; + var root = new LayoutTestRoot { Child = parent }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + child.DoArrangeOverride = (_, s) => + { + parent.InvalidateMeasure(); + return s; + }; + + child.InvalidateMeasure(); + parent.InvalidateMeasure(); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.True(child.IsMeasureValid); + Assert.True(child.IsArrangeValid); + Assert.True(parent.IsMeasureValid); + Assert.True(parent.IsArrangeValid); + } } } diff --git a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs index e9b82d5381..8eadb3a3f0 100644 --- a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs @@ -1,7 +1,9 @@ using System; +using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Platform; using Avalonia.Styling; +using Avalonia.Threading; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; using Moq; @@ -30,27 +32,23 @@ namespace Avalonia.Benchmarks.Themes _app.Dispose(); } - [Benchmark] - public void RepeatButton() + [Benchmark()] + [MethodImpl(MethodImplOptions.NoInlining)] + public void CreateButton() { - var button = new RepeatButton(); + var button = new Button(); _root.Child = button; _root.LayoutManager.ExecuteLayoutPass(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); } private static IDisposable CreateApp() { var services = new TestServices( - assetLoader: new AssetLoader(), - globalClock: new MockGlobalClock(), - platform: new AppBuilder().RuntimePlatform, - renderInterface: new MockPlatformRenderInterface(), - standardCursorFactory: Mock.Of(), - theme: () => LoadFluentTheme(), + renderInterface: new NullRenderingPlatform(), dispatcherImpl: new NullThreadingPlatform(), - fontManagerImpl: new MockFontManagerImpl(), - textShaperImpl: new MockTextShaperImpl(), - windowingPlatform: new MockWindowingPlatform()); + standardCursorFactory: new NullCursorFactory(), + theme: () => LoadFluentTheme()); return UnitTestApplication.Start(services); } diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index 7c0a3f8bdf..ac174e4bc2 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -1,5 +1,5 @@ using System; - +using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Markup.Xaml.Styling; using Avalonia.Platform; @@ -29,6 +29,7 @@ namespace Avalonia.Benchmarks.Themes } [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] public bool InitFluentTheme() { UnitTestApplication.Current.Styles[0] = new FluentTheme(); @@ -36,6 +37,7 @@ namespace Avalonia.Benchmarks.Themes } [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] public bool InitSimpleTheme() { UnitTestApplication.Current.Styles[0] = new SimpleTheme(); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 34311949ef..765f2d1c19 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -830,7 +830,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } }; } - window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + window.PlatformImpl?.Resize(new Size(700D, 500D), WindowResizeReason.Unspecified); Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); Assert.True(raised); } @@ -886,7 +886,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } }; } - window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + window.PlatformImpl?.Resize(new Size(700D, 500D), WindowResizeReason.Unspecified); Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); Assert.False(raised); } diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 62b5d889a8..0884dd306a 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -142,7 +142,7 @@ namespace Avalonia.Controls.UnitTests // The user has resized the window, so we can no longer auto-size. var target = new TestTopLevel(impl.Object); - impl.Object.Resized(new Size(100, 200), PlatformResizeReason.Unspecified); + impl.Object.Resized(new Size(100, 200), WindowResizeReason.Unspecified); Assert.Equal(100, target.Width); Assert.Equal(200, target.Height); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index cada2bfa6f..b59f6e03f7 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -704,8 +704,8 @@ namespace Avalonia.Controls.UnitTests var clientSize = new Size(200, 200); var maxClientSize = new Size(480, 480); - windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) - .Callback((size, reason) => + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) + .Callback((size, reason) => { clientSize = size.Constrain(maxClientSize); windowImpl.Object.Resized?.Invoke(clientSize, reason); @@ -853,7 +853,7 @@ namespace Avalonia.Controls.UnitTests target.PlatformImpl.ScalingChanged(1.5); target.PlatformImpl.Resized( new Size(210.66666666666666, 118.66666666666667), - PlatformResizeReason.DpiChange); + WindowResizeReason.DpiChange); Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); } @@ -911,7 +911,7 @@ namespace Avalonia.Controls.UnitTests target.LayoutManager.ExecuteLayoutPass(); var windowImpl = Mock.Get(target.PlatformImpl); - windowImpl.Verify(x => x.Resize(new Size(410, 800), PlatformResizeReason.Application)); + windowImpl.Verify(x => x.Resize(new Size(410, 800), WindowResizeReason.Application)); Assert.Equal(410, target.Width); } } @@ -936,7 +936,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(400, target.Width); Assert.Equal(800, target.Height); - target.PlatformImpl.Resized(new Size(410, 800), PlatformResizeReason.User); + target.PlatformImpl.Resized(new Size(410, 800), WindowResizeReason.User); Assert.Equal(410, target.Width); Assert.Equal(800, target.Height); @@ -963,7 +963,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(400, target.Width); Assert.Equal(800, target.Height); - target.PlatformImpl.Resized(new Size(400, 810), PlatformResizeReason.User); + target.PlatformImpl.Resized(new Size(400, 810), WindowResizeReason.User); Assert.Equal(400, target.Width); Assert.Equal(810, target.Height); @@ -991,7 +991,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(400, target.Width); Assert.Equal(800, target.Height); - target.PlatformImpl.Resized(new Size(410, 810), PlatformResizeReason.Unspecified); + target.PlatformImpl.Resized(new Size(410, 810), WindowResizeReason.Unspecified); Assert.Equal(400, target.Width); Assert.Equal(800, target.Height); diff --git a/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj b/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj new file mode 100644 index 0000000000..78a3ab186e --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + true + + + + + + + + + + + + + + diff --git a/tests/Avalonia.Headless.UnitTests/InputTests.cs b/tests/Avalonia.Headless.UnitTests/InputTests.cs new file mode 100644 index 0000000000..3c0ecbfdb7 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/InputTests.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Headless.UnitTests; + +public class InputTests +{ + [Fact] + public void Should_Click_Button_On_Window() + { + var buttonClicked = false; + var button = new Button + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch + }; + + button.Click += (_, _) => buttonClicked = true; + + var window = new Window + { + Width = 100, + Height = 100, + Content = button + }; + window.Show(); + + window.MouseDown(new Point(50, 50), MouseButton.Left); + window.MouseUp(new Point(50, 50), MouseButton.Left); + + Assert.True(buttonClicked); + } +} diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs new file mode 100644 index 0000000000..bc50686235 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs @@ -0,0 +1,33 @@ +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Headless.UnitTests; + +public class RenderingTests +{ + [Fact] + public void Should_Render_Last_Frame_To_Bitmap() + { + var window = new Window + { + Content = new ContentControl + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + Padding = new Thickness(4), + Content = new PathIcon + { + Data = StreamGeometry.Parse("M0,9 L10,0 20,9 19,10 10,2 1,10 z") + } + }, + SizeToContent = SizeToContent.WidthAndHeight + }; + window.Show(); + + var frame = window.CaptureRenderedFrame(); + Assert.NotNull(frame); + } +} diff --git a/tests/Avalonia.Headless.UnitTests/TestApplication.cs b/tests/Avalonia.Headless.UnitTests/TestApplication.cs new file mode 100644 index 0000000000..7bfa0144f3 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/TestApplication.cs @@ -0,0 +1,24 @@ +using Avalonia.Headless.UnitTests; +using Avalonia.Headless.XUnit; +using Avalonia.Themes.Simple; +using Xunit; + +[assembly: AvaloniaTestFramework(typeof(TestApplication))] +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Avalonia.Headless.UnitTests; + +public class TestApplication : Application +{ + public TestApplication() + { + Styles.Add(new SimpleTheme()); + } + + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .UseSkia() + .UseHeadless(new AvaloniaHeadlessPlatformOptions + { + UseHeadlessDrawing = false + }); +} diff --git a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs new file mode 100644 index 0000000000..419ee5519e --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Headless.UnitTests; + +public class ThreadingTests +{ + [Fact] + public void Should_Be_On_Dispatcher_Thread() + { + Dispatcher.UIThread.VerifyAccess(); + } + + [Fact] + public async Task DispatcherTimer_Works_On_The_Same_Thread() + { + var currentThread = Thread.CurrentThread; + var tcs = new TaskCompletionSource(); + + DispatcherTimer.RunOnce(() => + { + Assert.Equal(currentThread, Thread.CurrentThread); + + tcs.SetResult(); + }, TimeSpan.FromTicks(1)); + + await tcs.Task; + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/ScrollBarTests.cs b/tests/Avalonia.IntegrationTests.Appium/ScrollBarTests.cs index e9d0a5d3a4..9d5df2fb46 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ScrollBarTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ScrollBarTests.cs @@ -13,7 +13,7 @@ namespace Avalonia.IntegrationTests.Appium _session = fixture.Session; var tabs = _session.FindElementByAccessibilityId("MainTabs"); - var tab = tabs.FindElementByName("ScrollBarTab"); + var tab = tabs.FindElementByName("ScrollBar"); tab.Click(); } diff --git a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs index 9371a49ade..fa83ee199c 100644 --- a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Interactions; using Xunit; @@ -15,21 +16,95 @@ namespace Avalonia.IntegrationTests.Appium _session = fixture.Session; var tabs = _session.FindElementByAccessibilityId("MainTabs"); - var tab = tabs.FindElementByName("SliderTab"); + var tab = tabs.FindElementByName("Slider"); tab.Click(); + + var reset = _session.FindElementByAccessibilityId("ResetSliders"); + reset.Click(); + } + + [Fact] + public void Horizontal_Changes_Value_Dragging_Thumb_Right() + { + var slider = _session.FindElementByAccessibilityId("HorizontalSlider"); + var thumb = slider.FindElementByAccessibilityId("thumb"); + var initialThumbRect = thumb.Rect; + + new Actions(_session).ClickAndHold(thumb).MoveByOffset(100, 0).Release().Perform(); + + var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture)); + var boundValue = double.Parse( + _session.FindElementByAccessibilityId("HorizontalSliderValue").Text, + CultureInfo.InvariantCulture); + + Assert.True(value > 50); + Assert.Equal(value, boundValue); + + var currentThumbRect = thumb.Rect; + Assert.True(currentThumbRect.Left > initialThumbRect.Left); + } + + [Fact] + public void Horizontal_Changes_Value_Dragging_Thumb_Left() + { + var slider = _session.FindElementByAccessibilityId("HorizontalSlider"); + var thumb = slider.FindElementByAccessibilityId("thumb"); + var initialThumbRect = thumb.Rect; + + new Actions(_session).ClickAndHold(thumb).MoveByOffset(-100, 0).Release().Perform(); + + var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture)); + var boundValue = double.Parse( + _session.FindElementByAccessibilityId("HorizontalSliderValue").Text, + CultureInfo.InvariantCulture); + + Assert.True(value < 50); + Assert.Equal(value, boundValue); + + var currentThumbRect = thumb.Rect; + Assert.True(currentThumbRect.Left < initialThumbRect.Left); } [Fact] - public void Changes_Value_When_Clicking_Increase_Button() + public void Horizontal_Changes_Value_When_Clicking_Increase_Button() { - var slider = _session.FindElementByAccessibilityId("Slider"); + var slider = _session.FindElementByAccessibilityId("HorizontalSlider"); + var thumb = slider.FindElementByAccessibilityId("thumb"); + var initialThumbRect = thumb.Rect; + + new Actions(_session).MoveToElement(slider, 100, 0, MoveToElementOffsetOrigin.Center).Click().Perform(); + + var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture)); + var boundValue = double.Parse( + _session.FindElementByAccessibilityId("HorizontalSliderValue").Text, + CultureInfo.InvariantCulture); + + Assert.True(value > 50); + Assert.Equal(value, boundValue); + + var currentThumbRect = thumb.Rect; + Assert.True(currentThumbRect.Left > initialThumbRect.Left); + } + + [Fact] + public void Horizontal_Changes_Value_When_Clicking_Decrease_Button() + { + var slider = _session.FindElementByAccessibilityId("HorizontalSlider"); + var thumb = slider.FindElementByAccessibilityId("thumb"); + var initialThumbRect = thumb.Rect; + + new Actions(_session).MoveToElement(slider, -100, 0, MoveToElementOffsetOrigin.Center).Click().Perform(); - // slider.Text gets the Slider value - Assert.True(double.Parse(slider.Text) == 30); + var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture)); + var boundValue = double.Parse( + _session.FindElementByAccessibilityId("HorizontalSliderValue").Text, + CultureInfo.InvariantCulture); - new Actions(_session).Click(slider).Perform(); + Assert.True(value < 50); + Assert.Equal(value, boundValue); - Assert.Equal(50, Math.Round(double.Parse(slider.Text))); + var currentThumbRect = thumb.Rect; + Assert.True(currentThumbRect.Left < initialThumbRect.Left); } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 57d6a8902a..9f0b84733d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1978,7 +1978,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public bool Match(object data) => FancyDataType?.IsInstanceOfType(data) ?? true; - public Control Build(object data) => TemplateContent.Load(Content)?.Control; + public Control Build(object data) => TemplateContent.Load(Content)?.Result; } public class CustomDataTemplateInherit : CustomDataTemplate { } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 5e30198d00..421ed2c979 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -605,7 +605,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var control = new ContentControl(); - var result = (ContentPresenter)template.Build(control).Control; + var result = (ContentPresenter)template.Build(control).Result; Assert.NotNull(result); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs index 0a45814efe..4404564733 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs @@ -258,7 +258,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml "; var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - var parent = (ContentControl)template.Build(new ContentControl()).Control; + var parent = (ContentControl)template.Build(new ContentControl()).Result; Assert.Equal("parent", parent.Name); @@ -283,7 +283,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(typeof(ContentControl), template.TargetType); - Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Control); + Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Result); } [Fact] @@ -299,7 +299,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml "; var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - var panel = (Panel)template.Build(new ContentControl()).Control; + var panel = (Panel)template.Build(new ContentControl()).Result; Assert.Equal(2, panel.Children.Count); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs index aa76756069..d6f554cdfe 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs @@ -83,6 +83,34 @@ public class MergeResourceIncludeTests Assert.ThrowsAny(() => AvaloniaRuntimeXamlLoader.LoadGroup(documents)); } + [Fact] + public void MergeResourceInclude_Is_Allowed_After_ResourceInclude() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + Red +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + Blue +"), + new RuntimeXamlLoaderDocument(@" + + + + + +") + }; + + AvaloniaRuntimeXamlLoader.LoadGroup(documents); + } + [Fact] public void MergeResourceInclude_Works_With_Multiple_Resources() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs index 3ac4677694..2def84bb18 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs @@ -1,9 +1,12 @@ -using System.Linq; +using System; +using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Documents; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Styling; using Avalonia.UnitTests; @@ -140,7 +143,7 @@ public class ThemeDictionariesTests : XamlTestBase Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } - [Fact(Skip = "Not implemented")] + [Fact] public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" @@ -183,6 +186,135 @@ public class ThemeDictionariesTests : XamlTestBase Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } + + [Fact] + public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key_From_Inner_File() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Inner.xaml"), @" + + +"), + new RuntimeXamlLoaderDocument(@" + + + + Green + + + + + + White + + + + + +") + }; + + var parsed = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var dictionary = (ResourceDictionary)parsed[1]!; + + dictionary.TryGetResource("InnerKey", ThemeVariant.Dark, out var resource); + var colorResource = Assert.IsType(resource); + Assert.Equal(Colors.White, colorResource); + + dictionary.TryGetResource("InnerKey", ThemeVariant.Light, out resource); + colorResource = Assert.IsType(resource); + Assert.Equal(Colors.Green, colorResource); + } + + [Fact] + public void DynamicResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key_From_Inner_File() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Inner.xaml"), @" + + +"), + new RuntimeXamlLoaderDocument(@" + + + + Green + + + + + + White + + + + + +") + }; + + var parsed = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var dictionary1 = (ResourceDictionary)parsed[0]!; + var dictionary2 = (ResourceDictionary)parsed[1]!; + var ownerApp = new Application(); // DynamicResource needs an owner to work + ownerApp.RequestedThemeVariant = new ThemeVariant("FakeOne", null); + ownerApp.Resources.MergedDictionaries.Add(dictionary1); + ownerApp.Resources.MergedDictionaries.Add(dictionary2); + + dictionary2.TryGetResource("InnerKey", ThemeVariant.Dark, out var resource); + var colorResource = Assert.IsAssignableFrom(resource); + Assert.Equal(Colors.White, colorResource.Color); + + dictionary2.TryGetResource("InnerKey", ThemeVariant.Light, out resource); + colorResource = Assert.IsAssignableFrom(resource); + Assert.Equal(Colors.Green, colorResource.Color); + } + + [Fact] + public void DynamicResource_Inside_Control_Inside_Of_ThemeDictionaries_Should_Use_Control_Theme_Variant() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(@" + + + + Green + + + + White + + + +") + }; + + var parsed = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var dictionary = (ResourceDictionary)parsed[0]!; + + dictionary.TryGetResource("Template", ThemeVariant.Dark, out var resource); + var control = Assert.IsType((resource as Template)?.Build()); + control.Resources.MergedDictionaries.Add(dictionary); + Assert.Equal(Colors.Green, ((ISolidColorBrush)control[TextElement.ForegroundProperty]!).Color); + control.Resources.MergedDictionaries.Remove(dictionary); + + dictionary.TryGetResource("Template", ThemeVariant.Light, out resource); + control = Assert.IsType((resource as Template)?.Build()); + control.Resources.MergedDictionaries.Add(dictionary); + Assert.Equal(Colors.White, ((ISolidColorBrush)control[TextElement.ForegroundProperty]!).Color); + } [Fact] public void StaticResource_Outside_Of_Dictionaries_Should_Use_Control_ThemeVariant() diff --git a/tests/Avalonia.RenderTests/Shapes/PolygonTests.cs b/tests/Avalonia.RenderTests/Shapes/PolygonTests.cs index 3ac884df7d..b918f7180a 100644 --- a/tests/Avalonia.RenderTests/Shapes/PolygonTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PolygonTests.cs @@ -30,7 +30,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes Stroke = Brushes.DarkBlue, Stretch = Stretch.Uniform, Fill = Brushes.Violet, - Points = new [] { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) }, + Points = new Points { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) }, StrokeThickness = 1 } }; @@ -52,7 +52,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes Stroke = Brushes.DarkBlue, Stretch = Stretch.Fill, Fill = Brushes.Violet, - Points = new[] { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) }, + Points = new Points { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) }, StrokeThickness = 5, } }; diff --git a/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs b/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs index d02d494ff2..12420b524a 100644 --- a/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs @@ -20,7 +20,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes [Fact] public async Task Polyline_1px_Stroke() { - var polylinePoints = new Point[] { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3), + var polylinePoints = new Points { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3), new Point(9, 1), new Point(10, 0), new Point(15, 0) }; Decorator target = new Decorator @@ -44,7 +44,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes [Fact] public async Task Polyline_10px_Stroke_PenLineJoin() { - var polylinePoints = new Point[] { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3), + var polylinePoints = new Points { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3), new Point(9, 1), new Point(10, 0), new Point(15, 0) }; Decorator target = new Decorator diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs index 5ef09a4d0f..de7cbc873c 100644 --- a/tests/Avalonia.UnitTests/CompositorTestServices.cs +++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs @@ -152,7 +152,7 @@ public class CompositorTestServices : IDisposable public IEnumerable Surfaces { get; } = new[] { new DummyFramebufferSurface() }; public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 142a9cd8ee..ca71a97a6e 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives.PopupPositioning; using Moq; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Controls; namespace Avalonia.UnitTests { @@ -54,8 +55,8 @@ namespace Avalonia.UnitTests windowImpl.Object.PositionChanged?.Invoke(x); }); - windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) - .Callback((x, y) => + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) + .Callback((x, y) => { var constrainedSize = x.Constrain(s_screenSize); @@ -68,7 +69,7 @@ namespace Avalonia.UnitTests windowImpl.Setup(x => x.Show(true, It.IsAny())).Callback(() => { - windowImpl.Object.Resized?.Invoke(windowImpl.Object.ClientSize, PlatformResizeReason.Unspecified); + windowImpl.Object.Resized?.Invoke(windowImpl.Object.ClientSize, WindowResizeReason.Unspecified); windowImpl.Object.Activated?.Invoke(); }); @@ -87,7 +88,7 @@ namespace Avalonia.UnitTests { clientSize = size.Constrain(s_screenSize); popupImpl.Object.PositionChanged?.Invoke(pos); - popupImpl.Object.Resized?.Invoke(clientSize, PlatformResizeReason.Unspecified); + popupImpl.Object.Resized?.Invoke(clientSize, WindowResizeReason.Unspecified); }); var positioner = new ManagedPopupPositioner(positionerHelper);