diff --git a/Avalonia.sln b/Avalonia.sln index a989fb828d..a792774d94 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -234,6 +234,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -2212,6 +2214,30 @@ Global {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhone.Build.0 = Release|Any CPU {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhone.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhone.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|Any CPU.Build.0 = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.ActiveCfg = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.Build.0 = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2276,6 +2302,7 @@ Global {26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098} {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {CE910927-CE5A-456F-BC92-E4C757354A5C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/native/Avalonia.Native/src/OSX/controlhost.mm b/native/Avalonia.Native/src/OSX/controlhost.mm index f8e9a3b6d1..5683a5a975 100644 --- a/native/Avalonia.Native/src/OSX/controlhost.mm +++ b/native/Avalonia.Native/src/OSX/controlhost.mm @@ -36,7 +36,10 @@ public: virtual void DestroyDefaultChild(void* child) override { // ARC will release the object for us + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wunused-value" (__bridge_transfer NSView*) child; + #pragma clang diagnostic pop } }; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 9b703c4838..620b750a40 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -457,7 +457,8 @@ public: } point = ConvertPointY(point); - auto viewPoint = [Window convertScreenToBase:ToNSPoint(point)]; + NSRect convertRect = [Window convertRectToScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; + auto viewPoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y); *ret = [View translateLocalPoint:ToAvnPoint(viewPoint)]; @@ -477,7 +478,8 @@ public: } auto cocoaViewPoint = ToNSPoint([View translateLocalPoint:point]); - auto cocoaScreenPoint = [Window convertBaseToScreen:cocoaViewPoint]; + NSRect convertRect = [Window convertRectToScreen:NSMakeRect(cocoaViewPoint.x, cocoaViewPoint.y, 0.0, 0.0)]; + auto cocoaScreenPoint = NSPointFromCGPoint(NSMakePoint(convertRect.origin.x, convertRect.origin.y)); *ret = ConvertPointY(ToAvnPoint(cocoaScreenPoint)); return S_OK; @@ -573,7 +575,8 @@ public: if(!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited) || (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged))) { - auto nspoint = [Window convertBaseToScreen: ToNSPoint(point)]; + NSRect convertRect = [Window convertRectToScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; + auto nspoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y); CGPoint cgpoint = NSPointToCGPoint(nspoint); auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft); nsevent = [NSEvent eventWithCGEvent: cgevent]; diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 4b28527465..4d0ed866a3 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -8,7 +8,9 @@ all - + true + TargetFramework=netstandard2.0 + diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 516acfe4b9..9777bb46c3 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -21,8 +21,9 @@ True - False + True True + True diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index e96b7aff08..3cadc7243a 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -157,7 +157,8 @@ namespace ControlCatalog.Pages (button = new Button { HorizontalAlignment = HorizontalAlignment.Center, - Content = "Click to close" + Content = "Click to close", + IsDefault = true }) } }, diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 34784612f1..5343b57251 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -2,18 +2,22 @@ using System; using Android.Content; using Android.Graphics; using Android.OS; +using Android.Runtime; using Android.Util; using Android.Views; +using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Platform; namespace Avalonia.Android { - public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformHandle + public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformNativeSurfaceHandle { bool _invalidateQueued; readonly object _lock = new object(); private readonly Handler _handler; - + + IntPtr IPlatformHandle.Handle => + AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, Holder.Surface.Handle); public InvalidationAwareSurfaceView(Context context) : base(context) { @@ -25,7 +29,7 @@ namespace Avalonia.Android { lock (_lock) { - if(_invalidateQueued) + if (_invalidateQueued) return; _handler.Post(() => { @@ -70,7 +74,7 @@ namespace Avalonia.Android public void SurfaceDestroyed(ISurfaceHolder holder) { Log.Info("AVALONIA", "Surface Destroyed"); - + } protected void DoDraw() @@ -83,5 +87,9 @@ namespace Avalonia.Android } protected abstract void Draw(); public string HandleDescriptor => "SurfaceView"; + + public PixelSize Size => new PixelSize(Holder.SurfaceFrame.Width(), Holder.SurfaceFrame.Height()); + + public double Scaling => Resources.DisplayMetrics.Density; } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 0afb1db141..8a475676a5 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using Android.Content; using Android.Graphics; -using Android.Runtime; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.OpenGL; @@ -38,11 +37,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform _keyboardHelper = new AndroidKeyboardEventsHelper(this); _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, GetAvaloniaPointFromEvent); - _gl = GlPlatformSurface.TryCreate(this); _framebuffer = new FramebufferManager(this); - RenderScaling = (int)_view.Resources.DisplayMetrics.Density; + RenderScaling = (int)_view.Scaling; MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); @@ -77,7 +75,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IPlatformHandle Handle => _view; - public IEnumerable Surfaces => new object[] { _gl, _framebuffer }; + public IEnumerable Surfaces => new object[] { _gl, _framebuffer, Handle }; public IRenderer CreateRenderer(IRenderRoot root) => AndroidPlatform.Options.UseDeferredRendering @@ -216,10 +214,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); - IntPtr EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo.Handle => - AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, _view.Holder.Surface.Handle); + IntPtr EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle; - public PixelSize Size => new PixelSize(_view.Holder.SurfaceFrame.Width(), _view.Holder.SurfaceFrame.Height()); + public PixelSize Size => _view.Size; public double Scaling => RenderScaling; diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index 8cb7aa1cfd..db0bb01410 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -11,12 +11,18 @@ true portable - + + + + Resources\drawable\Icon.png + + + True True True - + True diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs index 471b982d9e..8f4beb2737 100644 --- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs +++ b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs @@ -1,9 +1,10 @@ using System; using Android.App; using Android.Content.PM; -using Android.OS; using Avalonia.Android; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Input.TextInput; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Styling; @@ -14,22 +15,15 @@ namespace Avalonia.AndroidTestApplication [Activity(Label = "Main", MainLauncher = true, Icon = "@drawable/icon", - Theme = "@style/Theme.AppCompat.NoActionBar", - ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, + Theme = "@style/Theme.AppCompat.NoActionBar", + ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, LaunchMode = LaunchMode.SingleInstance/*, ScreenOrientation = ScreenOrientation.Landscape*/)] - public class MainBaseActivity : AvaloniaActivity + public class MainActivity : AvaloniaActivity { - protected override void OnCreate(Bundle savedInstanceState) + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) { - if (Avalonia.Application.Current == null) - { - AppBuilder.Configure() - .UseAndroid() - .SetupWithoutStarting(); - } - base.OnCreate(savedInstanceState); - Content = App.CreateSimpleWindow(); + return base.CustomizeAppBuilder(builder); } } @@ -37,13 +31,17 @@ namespace Avalonia.AndroidTestApplication { public override void Initialize() { - Styles.Add(new DefaultTheme()); - - var baseLight = (IStyle)AvaloniaXamlLoader.Load( - new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default")); - Styles.Add(baseLight); + Styles.Add(new SimpleTheme(new Uri("avares://Avalonia.AndroidTestApplication"))); + } + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) + { + singleViewLifetime.MainView = CreateSimpleWindow(); + } + base.OnFrameworkInitializationCompleted(); } // This provides a simple UI tree for testing input handling, drawing, etc @@ -76,12 +74,12 @@ namespace Avalonia.AndroidTestApplication Foreground = Brushes.Black }, - CreateTextBox(Input.TextInput.TextInputContentType.Normal), - CreateTextBox(Input.TextInput.TextInputContentType.Password), - CreateTextBox(Input.TextInput.TextInputContentType.Email), - CreateTextBox(Input.TextInput.TextInputContentType.Url), - CreateTextBox(Input.TextInput.TextInputContentType.Phone), - CreateTextBox(Input.TextInput.TextInputContentType.Number), + CreateTextBox(TextInputContentType.Normal), + CreateTextBox(TextInputContentType.Password), + CreateTextBox(TextInputContentType.Email), + CreateTextBox(TextInputContentType.Url), + CreateTextBox(TextInputContentType.Digits), + CreateTextBox(TextInputContentType.Number), } } }; @@ -89,16 +87,16 @@ namespace Avalonia.AndroidTestApplication return window; } - private static TextBox CreateTextBox(Input.TextInput.TextInputContentType contentType) + private static TextBox CreateTextBox(TextInputContentType contentType) { var textBox = new TextBox() { Margin = new Thickness(20, 10), Watermark = contentType.ToString(), BorderThickness = new Thickness(3), - FontSize = 20 + FontSize = 20, + [TextInputOptions.ContentTypeProperty] = contentType }; - textBox.TextInputOptionsQuery += (s, e) => { e.ContentType = contentType; }; return textBox; } diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index 053c7a7547..9ffb5872f0 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -11,4 +11,5 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Visuals, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.PlatformSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] diff --git a/src/Avalonia.Base/Utilities/UriExtensions.cs b/src/Avalonia.Base/Utilities/UriExtensions.cs new file mode 100644 index 0000000000..c706f72a63 --- /dev/null +++ b/src/Avalonia.Base/Utilities/UriExtensions.cs @@ -0,0 +1,70 @@ +using System; + +namespace Avalonia.Utilities; + +internal static class UriExtensions +{ + public static bool IsAbsoluteResm(this Uri uri) => + uri.IsAbsoluteUri && uri.IsResm(); + + public static bool IsResm(this Uri uri) => uri.Scheme == "resm"; + + public static bool IsAvares(this Uri uri) => uri.Scheme == "avares"; + + public static Uri EnsureAbsolute(this Uri uri, Uri? baseUri) + { + if (uri.IsAbsoluteUri) + return uri; + if(baseUri == null) + throw new ArgumentException($"Relative uri {uri} without base url"); + if (!baseUri.IsAbsoluteUri) + throw new ArgumentException($"Base uri {baseUri} is relative"); + if (baseUri.IsResm()) + throw new ArgumentException( + $"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm"); + return new Uri(baseUri, uri); + } + + public static string GetUnescapeAbsolutePath(this Uri uri) => + Uri.UnescapeDataString(uri.AbsolutePath); + + public static string GetUnescapeAbsoluteUri(this Uri uri) => + Uri.UnescapeDataString(uri.AbsoluteUri); + + public static string GetAssemblyNameFromQuery(this Uri uri) + { + const string assembly = "assembly"; + + var query = Uri.UnescapeDataString(uri.Query); + + // Skip the '?' + var currentIndex = 1; + while (currentIndex < query.Length) + { + var isFind = false; + for (var i = 0; i < assembly.Length; ++currentIndex, ++i) + if (query[currentIndex] == assembly[i]) + { + isFind = i == assembly.Length - 1; + } + else + { + break; + } + + // Skip the '=' + ++currentIndex; + + var beginIndex = currentIndex; + while (currentIndex < query.Length && query[currentIndex] != '&') + ++currentIndex; + + if (isFind) + return query.Substring(beginIndex, currentIndex - beginIndex); + + ++currentIndex; + } + + return ""; + } +} diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 72495bdcb3..899521536f 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -539,6 +539,7 @@ namespace Avalonia.Controls if (e.Key == Key.Enter && IsVisible && IsEnabled) { OnClick(); + e.Handled = true; } } @@ -552,6 +553,7 @@ namespace Avalonia.Controls if (e.Key == Key.Escape && IsVisible && IsEnabled) { OnClick(); + e.Handled = true; } } diff --git a/src/Avalonia.Controls/Platform/IPlatformNativeSurfaceHandle.cs b/src/Avalonia.Controls/Platform/IPlatformNativeSurfaceHandle.cs new file mode 100644 index 0000000000..264f5e4667 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IPlatformNativeSurfaceHandle.cs @@ -0,0 +1,10 @@ +using System; + +namespace Avalonia.Platform +{ + public interface IPlatformNativeSurfaceHandle : IPlatformHandle + { + PixelSize Size { get; } + double Scaling { get; } + } +} diff --git a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj index b796e173c4..d7f39f6642 100644 --- a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj +++ b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj @@ -10,6 +10,8 @@ false all + true + TargetFramework=netstandard2.0 diff --git a/src/Avalonia.PlatformSupport/AssetLoader.cs b/src/Avalonia.PlatformSupport/AssetLoader.cs index 7220694d7b..fb03ec2f6e 100644 --- a/src/Avalonia.PlatformSupport/AssetLoader.cs +++ b/src/Avalonia.PlatformSupport/AssetLoader.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Reflection; using Avalonia.Platform; +using Avalonia.PlatformSupport.Internal; using Avalonia.Utilities; namespace Avalonia.PlatformSupport @@ -13,12 +14,16 @@ namespace Avalonia.PlatformSupport /// public class AssetLoader : IAssetLoader { - private const string AvaloniaResourceName = "!AvaloniaResources"; - private static readonly Dictionary AssemblyNameCache - = new Dictionary(); + private static AssemblyDescriptorResolver s_assemblyDescriptorResolver = new(); private AssemblyDescriptor? _defaultResmAssembly; + /// + /// Introduced for tests. + /// + internal static void SetAssemblyDescriptorResolver(AssemblyDescriptorResolver resolver) => + s_assemblyDescriptorResolver = resolver; + /// /// Initializes a new instance of the class. /// @@ -109,17 +114,18 @@ namespace Avalonia.PlatformSupport /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset public IEnumerable GetAssets(Uri uri, Uri? baseUri) { - if (uri.IsAbsoluteUri && uri.Scheme == "resm") + if (uri.IsAbsoluteResm()) { var assembly = GetAssembly(uri); - return assembly?.Resources?.Where(x => x.Key.Contains(uri.AbsolutePath)) + return assembly?.Resources? + .Where(x => x.Key.IndexOf(uri.GetUnescapeAbsolutePath(), StringComparison.Ordinal) >= 0) .Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? Enumerable.Empty(); } - uri = EnsureAbsolute(uri, baseUri); - if (uri.Scheme == "avares") + uri = uri.EnsureAbsolute(baseUri); + if (uri.IsAvares()) { var (asm, path) = GetResAsmAndPath(uri); if (asm == null) @@ -129,33 +135,23 @@ namespace Avalonia.PlatformSupport "don't know where to look up for the resource, try specifying assembly explicitly."); } - if (asm?.AvaloniaResources == null) + if (asm.AvaloniaResources == null) return Enumerable.Empty(); - path = path.TrimEnd('/') + '/'; - return asm.AvaloniaResources.Where(r => r.Key.StartsWith(path)) + + if (path[path.Length - 1] != '/') + path += '/'; + + return asm.AvaloniaResources + .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) .Select(x => new Uri($"avares://{asm.Name}{x.Key}")); } return Enumerable.Empty(); } - - private Uri EnsureAbsolute(Uri uri, Uri? baseUri) - { - if (uri.IsAbsoluteUri) - return uri; - if(baseUri == null) - throw new ArgumentException($"Relative uri {uri} without base url"); - if (!baseUri.IsAbsoluteUri) - throw new ArgumentException($"Base uri {baseUri} is relative"); - if (baseUri.Scheme == "resm") - throw new ArgumentException( - $"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm"); - return new Uri(baseUri, uri); - } private IAssetDescriptor? GetAsset(Uri uri, Uri? baseUri) { - if (uri.IsAbsoluteUri && uri.Scheme == "resm") + if (uri.IsAbsoluteResm()) { var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly; @@ -172,9 +168,9 @@ namespace Avalonia.PlatformSupport return rv; } - uri = EnsureAbsolute(uri, baseUri); + uri = uri.EnsureAbsolute(baseUri); - if (uri.Scheme == "avares") + if (uri.IsAvares()) { var (asm, path) = GetResAsmAndPath(uri); if (asm.AvaloniaResources == null) @@ -188,8 +184,8 @@ namespace Avalonia.PlatformSupport private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) { - var asm = GetAssembly(uri.Authority); - return (asm, uri.AbsolutePath); + var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority); + return (asm, uri.GetUnescapeAbsolutePath()); } private AssemblyDescriptor? GetAssembly(Uri? uri) @@ -198,197 +194,20 @@ namespace Avalonia.PlatformSupport { if (!uri.IsAbsoluteUri) return null; - if (uri.Scheme == "avares") + if (uri.IsAvares()) return GetResAsmAndPath(uri).asm; - if (uri.Scheme == "resm") + if (uri.IsResm()) { - var qs = ParseQueryString(uri); - - if (qs.TryGetValue("assembly", out var assemblyName)) - { - return GetAssembly(assemblyName); - } + var assemblyName = uri.GetAssemblyNameFromQuery(); + if (assemblyName.Length > 0) + return s_assemblyDescriptorResolver.GetAssembly(assemblyName); } } return null; } - private AssemblyDescriptor GetAssembly(string name) - { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - if (!AssemblyNameCache.TryGetValue(name, out var rv)) - { - var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - var match = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == name); - if (match != null) - { - AssemblyNameCache[name] = rv = new AssemblyDescriptor(match); - } - else - { - // iOS does not support loading assemblies dynamically! -#if NET6_0_OR_GREATER - if (OperatingSystem.IsIOS()) - { - throw new InvalidOperationException( - $"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); - } -#endif - name = Uri.UnescapeDataString(name); - AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name)); - } - } - - return rv; - } - - private Dictionary ParseQueryString(Uri uri) - { - return uri.Query.TrimStart('?') - .Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries) - .Select(p => p.Split('=')) - .ToDictionary(p => p[0], p => p[1]); - } - - private interface IAssetDescriptor - { - Stream GetStream(); - Assembly Assembly { get; } - } - - private class AssemblyResourceDescriptor : IAssetDescriptor - { - private readonly Assembly _asm; - private readonly string _name; - - public AssemblyResourceDescriptor(Assembly asm, string name) - { - _asm = asm; - _name = name; - } - - public Stream GetStream() - { - var s = _asm.GetManifestResourceStream(_name); - return s ?? throw new InvalidOperationException($"Could not find manifest resource stream '{_name}',"); - } - - public Assembly Assembly => _asm; - } - - private class AvaloniaResourceDescriptor : IAssetDescriptor - { - private readonly int _offset; - private readonly int _length; - public Assembly Assembly { get; } - - public AvaloniaResourceDescriptor(Assembly asm, int offset, int length) - { - _offset = offset; - _length = length; - Assembly = asm; - } - - public Stream GetStream() - { - var s = Assembly.GetManifestResourceStream(AvaloniaResourceName) ?? - throw new InvalidOperationException($"Could not find manifest resource stream '{AvaloniaResourceName}',"); - return new SlicedStream(s, _offset, _length); - } - } - - class SlicedStream : Stream - { - private readonly Stream _baseStream; - private readonly int _from; - - public SlicedStream(Stream baseStream, int from, int length) - { - Length = length; - _baseStream = baseStream; - _from = from; - _baseStream.Position = from; - } - public override void Flush() - { - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _baseStream.Read(buffer, offset, (int)Math.Min(count, Length - Position)); - } - - public override long Seek(long offset, SeekOrigin origin) - { - if (origin == SeekOrigin.Begin) - Position = offset; - if (origin == SeekOrigin.End) - Position = _from + Length + offset; - if (origin == SeekOrigin.Current) - Position = Position + offset; - return Position; - } - - public override void SetLength(long value) => throw new NotSupportedException(); - - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - - public override bool CanRead => true; - public override bool CanSeek => _baseStream.CanRead; - public override bool CanWrite => false; - public override long Length { get; } - public override long Position - { - get => _baseStream.Position - _from; - set => _baseStream.Position = value + _from; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - _baseStream.Dispose(); - } - - public override void Close() => _baseStream.Close(); - } - - private class AssemblyDescriptor - { - public AssemblyDescriptor(Assembly assembly) - { - Assembly = assembly; - - if (assembly != null) - { - Resources = assembly.GetManifestResourceNames() - .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); - Name = assembly.GetName().Name; - using (var resources = assembly.GetManifestResourceStream(AvaloniaResourceName)) - { - if (resources != null) - { - Resources.Remove(AvaloniaResourceName); - - var indexLength = new BinaryReader(resources).ReadInt32(); - var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); - var baseOffset = indexLength + 4; - AvaloniaResources = index.ToDictionary(r => "/" + r.Path!.TrimStart('/'), r => (IAssetDescriptor) - new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); - } - } - } - } - - public Assembly Assembly { get; } - public Dictionary? Resources { get; } - public Dictionary? AvaloniaResources { get; } - public string? Name { get; } - } - public static void RegisterResUriParsers() { if (!UriParser.IsKnownScheme("avares")) diff --git a/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj b/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj index e08dc5e194..420ac0796c 100644 --- a/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj +++ b/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj @@ -16,4 +16,9 @@ + + + + + diff --git a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs new file mode 100644 index 0000000000..a3de7f2b8a --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Avalonia.Utilities; + +namespace Avalonia.PlatformSupport.Internal; + +internal class AssemblyDescriptor +{ + public AssemblyDescriptor(Assembly assembly) + { + Assembly = assembly; + + if (assembly != null) + { + Resources = assembly.GetManifestResourceNames() + .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); + Name = assembly.GetName().Name; + using (var resources = assembly.GetManifestResourceStream(Constants.AvaloniaResourceName)) + { + if (resources != null) + { + Resources.Remove(Constants.AvaloniaResourceName); + + var indexLength = new BinaryReader(resources).ReadInt32(); + var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); + var baseOffset = indexLength + 4; + AvaloniaResources = index.ToDictionary(r => GetPathRooted(r), r => (IAssetDescriptor) + new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); + } + } + } + } + + public Assembly Assembly { get; } + public Dictionary? Resources { get; } + public Dictionary? AvaloniaResources { get; } + public string? Name { get; } + private static string GetPathRooted(AvaloniaResourcesIndexEntry r) => + r.Path![0] == '/' ? r.Path : '/' + r.Path; +} diff --git a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs new file mode 100644 index 0000000000..a78051a9c4 --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Avalonia.PlatformSupport.Internal; + +internal class AssemblyDescriptorResolver +{ + private readonly Dictionary _assemblyNameCache = new(); + + public AssemblyDescriptor GetAssembly(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (!_assemblyNameCache.TryGetValue(name, out var rv)) + { + var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + var match = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == name); + if (match != null) + { + _assemblyNameCache[name] = rv = new AssemblyDescriptor(match); + } + else + { + // iOS does not support loading assemblies dynamically! +#if NET6_0_OR_GREATER + if (OperatingSystem.IsIOS()) + { + throw new InvalidOperationException( + $"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); + } +#endif + name = Uri.UnescapeDataString(name); + _assemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name)); + } + } + + return rv; + } +} diff --git a/src/Avalonia.PlatformSupport/Internal/AssetDescriptor.cs b/src/Avalonia.PlatformSupport/Internal/AssetDescriptor.cs new file mode 100644 index 0000000000..baae1f99e7 --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/AssetDescriptor.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Avalonia.PlatformSupport.Internal; + +internal interface IAssetDescriptor +{ + Stream GetStream(); + Assembly Assembly { get; } +} + +internal class AssemblyResourceDescriptor : IAssetDescriptor +{ + private readonly Assembly _asm; + private readonly string _name; + + public AssemblyResourceDescriptor(Assembly asm, string name) + { + _asm = asm; + _name = name; + } + + public Stream GetStream() + { + var s = _asm.GetManifestResourceStream(_name); + return s ?? throw new InvalidOperationException($"Could not find manifest resource stream '{_name}',"); + } + + public Assembly Assembly => _asm; +} + +internal class AvaloniaResourceDescriptor : IAssetDescriptor +{ + private readonly int _offset; + private readonly int _length; + public Assembly Assembly { get; } + + public AvaloniaResourceDescriptor(Assembly asm, int offset, int length) + { + _offset = offset; + _length = length; + Assembly = asm; + } + + public Stream GetStream() + { + var s = Assembly.GetManifestResourceStream(Constants.AvaloniaResourceName) ?? + throw new InvalidOperationException($"Could not find manifest resource stream '{Constants.AvaloniaResourceName}',"); + return new SlicedStream(s, _offset, _length); + } +} diff --git a/src/Avalonia.PlatformSupport/Internal/Constants.cs b/src/Avalonia.PlatformSupport/Internal/Constants.cs new file mode 100644 index 0000000000..c8a0f7b1ce --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/Constants.cs @@ -0,0 +1,6 @@ +namespace Avalonia.PlatformSupport.Internal; + +internal static class Constants +{ + public static string AvaloniaResourceName => "!AvaloniaResources"; +} diff --git a/src/Avalonia.PlatformSupport/Internal/SlicedStream.cs b/src/Avalonia.PlatformSupport/Internal/SlicedStream.cs new file mode 100644 index 0000000000..e310db964a --- /dev/null +++ b/src/Avalonia.PlatformSupport/Internal/SlicedStream.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; + +namespace Avalonia.PlatformSupport.Internal; + +internal class SlicedStream : Stream +{ + private readonly Stream _baseStream; + private readonly int _from; + + public SlicedStream(Stream baseStream, int from, int length) + { + Length = length; + _baseStream = baseStream; + _from = from; + _baseStream.Position = from; + } + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _baseStream.Read(buffer, offset, (int)Math.Min(count, Length - Position)); + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + Position = offset; + if (origin == SeekOrigin.End) + Position = _from + Length + offset; + if (origin == SeekOrigin.Current) + Position = Position + offset; + return Position; + } + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override bool CanRead => true; + public override bool CanSeek => _baseStream.CanRead; + public override bool CanWrite => false; + public override long Length { get; } + public override long Position + { + get => _baseStream.Position - _from; + set => _baseStream.Position = value + _from; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + _baseStream.Dispose(); + } + + public override void Close() => _baseStream.Close(); +} diff --git a/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml index 305f1b1814..2c831cf360 100644 --- a/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml @@ -254,7 +254,7 @@