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
-
+
+
+
+ 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 @@