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/Converter/MathSubtractConverter.cs b/samples/ControlCatalog/Converter/MathSubtractConverter.cs
new file mode 100644
index 0000000000..009a56c0ec
--- /dev/null
+++ b/samples/ControlCatalog/Converter/MathSubtractConverter.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace ControlCatalog.Converter;
+
+public class MathSubtractConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return (double)value - (double)parameter;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+}
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index facce2aa82..f3b52428ca 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -145,6 +145,9 @@
+
+
+
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/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
new file mode 100644
index 0000000000..a410577d27
--- /dev/null
+++ b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 8
+
+
+
+
+ The TransitioningContentControl control allows you to show a page transition whenever the Content changes.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs
new file mode 100644
index 0000000000..2851a6c890
--- /dev/null
+++ b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public partial class TransitioningContentControlPage : UserControl
+ {
+ public TransitioningContentControlPage()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs b/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
new file mode 100644
index 0000000000..b092a07f4a
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
@@ -0,0 +1,309 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using Avalonia.Styling;
+using Avalonia.VisualTree;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+ public class TransitioningContentControlPageViewModel : ViewModelBase
+ {
+ public TransitioningContentControlPageViewModel()
+ {
+ var assetLoader = AvaloniaLocator.Current.GetService();
+
+ var images = new string[]
+ {
+ "delicate-arch-896885_640.jpg",
+ "hirsch-899118_640.jpg",
+ "maple-leaf-888807_640.jpg"
+ };
+
+ foreach (var image in images)
+ {
+ var path = $"avares://ControlCatalog/Assets/{image}";
+ Images.Add(new Bitmap(assetLoader.Open(new Uri(path))));
+ }
+
+ SetupTransitions();
+
+ SelectedTransition = PageTransitions[1];
+ SelectedImage = Images[0];
+ }
+
+ public List PageTransitions { get; } = new List();
+
+ public List Images { get; } = new List();
+
+
+ private Bitmap? _SelectedImage;
+
+ ///
+ /// Gets or Sets the selected image
+ ///
+ public Bitmap? SelectedImage
+ {
+ get { return _SelectedImage; }
+ set { this.RaiseAndSetIfChanged(ref _SelectedImage, value); }
+ }
+
+
+ private PageTransition _SelectedTransition;
+
+ ///
+ /// Gets or sets the transition to play
+ ///
+ public PageTransition SelectedTransition
+ {
+ get { return _SelectedTransition; }
+ set { this.RaiseAndSetIfChanged(ref _SelectedTransition, value); }
+ }
+
+
+
+ private bool _ClipToBounds;
+
+ ///
+ /// Gets or sets if the content should be clipped to bounds
+ ///
+ public bool ClipToBounds
+ {
+ get { return _ClipToBounds; }
+ set { this.RaiseAndSetIfChanged(ref _ClipToBounds, value); }
+ }
+
+
+ private int _Duration = 500;
+
+ ///
+ /// Gets or Sets the duration
+ ///
+ public int Duration
+ {
+ get { return _Duration; }
+ set
+ {
+ this.RaiseAndSetIfChanged(ref _Duration , value);
+ SetupTransitions();
+ }
+ }
+
+ private void SetupTransitions()
+ {
+ if (PageTransitions.Count == 0)
+ {
+ PageTransitions.AddRange(new[]
+ {
+ new PageTransition("None"),
+ new PageTransition("CrossFade"),
+ new PageTransition("Slide horizontally"),
+ new PageTransition("Slide vertically"),
+ new PageTransition("Composite"),
+ new PageTransition("Custom")
+ });
+ }
+
+ PageTransitions[1].Transition = new CrossFade(TimeSpan.FromMilliseconds(Duration));
+ PageTransitions[2].Transition = new PageSlide(TimeSpan.FromMilliseconds(Duration), PageSlide.SlideAxis.Horizontal);
+ PageTransitions[3].Transition = new PageSlide(TimeSpan.FromMilliseconds(Duration), PageSlide.SlideAxis.Vertical);
+
+ var compositeTransition = new CompositePageTransition();
+ compositeTransition.PageTransitions.Add(PageTransitions[1].Transition);
+ compositeTransition.PageTransitions.Add(PageTransitions[2].Transition);
+ compositeTransition.PageTransitions.Add(PageTransitions[3].Transition);
+ PageTransitions[4].Transition = compositeTransition;
+
+ PageTransitions[5].Transition = new CustomTransition(TimeSpan.FromMilliseconds(Duration));
+ }
+
+ public void NextImage()
+ {
+ var index = Images.IndexOf(SelectedImage) + 1;
+
+ if (index >= Images.Count)
+ {
+ index = 0;
+ }
+
+ SelectedImage = Images[index];
+ }
+
+ public void PrevImage()
+ {
+ var index = Images.IndexOf(SelectedImage) - 1;
+
+ if (index < 0)
+ {
+ index = Images.Count-1;
+ }
+
+ SelectedImage = Images[index];
+ }
+ }
+
+ public class PageTransition : ViewModelBase
+ {
+ public PageTransition(string displayTitle)
+ {
+ DisplayTitle = displayTitle;
+ }
+
+ public string DisplayTitle { get; }
+
+
+ private IPageTransition _Transition;
+
+ ///
+ /// Gets or sets the transition
+ ///
+ public IPageTransition Transition
+ {
+ get { return _Transition; }
+ set { this.RaiseAndSetIfChanged(ref _Transition, value); }
+ }
+
+ public override string ToString()
+ {
+ return DisplayTitle;
+ }
+
+ }
+
+ public class CustomTransition : IPageTransition
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CustomTransition()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public CustomTransition(TimeSpan duration)
+ {
+ Duration = duration;
+ }
+
+ ///
+ /// Gets the duration of the animation.
+ ///
+ public TimeSpan Duration { get; set; }
+
+ public async Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ var tasks = new List();
+ var parent = GetVisualParent(from, to);
+ var scaleProperty = ScaleTransform.ScaleYProperty;
+
+ if (from != null)
+ {
+ var animation = new Animation
+ {
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ new KeyFrame
+ {
+ Setters = { new Setter { Property = scaleProperty, Value = 1d } },
+ Cue = new Cue(0d)
+ },
+ new KeyFrame
+ {
+ Setters =
+ {
+ new Setter
+ {
+ Property = scaleProperty,
+ Value = 0d
+ }
+ },
+ Cue = new Cue(1d)
+ }
+ },
+ Duration = Duration
+ };
+ tasks.Add(animation.RunAsync(from, null, cancellationToken));
+ }
+
+ if (to != null)
+ {
+ to.IsVisible = true;
+ var animation = new Animation
+ {
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ new KeyFrame
+ {
+ Setters =
+ {
+ new Setter
+ {
+ Property = scaleProperty,
+ Value = 0d
+ }
+ },
+ Cue = new Cue(0d)
+ },
+ new KeyFrame
+ {
+ Setters = { new Setter { Property = scaleProperty, Value = 1d } },
+ Cue = new Cue(1d)
+ }
+ },
+ Duration = Duration
+ };
+ tasks.Add(animation.RunAsync(to, null, cancellationToken));
+ }
+
+ await Task.WhenAll(tasks);
+
+ if (from != null && !cancellationToken.IsCancellationRequested)
+ {
+ from.IsVisible = false;
+ }
+ }
+
+ ///
+ /// Gets the common visual parent of the two control.
+ ///
+ /// The from control.
+ /// The to control.
+ /// The common parent.
+ ///
+ /// The two controls do not share a common parent.
+ ///
+ ///
+ /// Any one of the parameters may be null, but not both.
+ ///
+ private static IVisual GetVisualParent(IVisual? from, IVisual? to)
+ {
+ var p1 = (from ?? to)!.VisualParent;
+ var p2 = (to ?? from)!.VisualParent;
+
+ if (p1 != null && p2 != null && p1 != p2)
+ {
+ throw new ArgumentException("Controls for PageSlide must have same parent.");
+ }
+
+ return p1 ?? throw new InvalidOperationException("Cannot determine visual parent.");
+ }
+ }
+}
diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
index 1aa7f8ea04..e0dfa49a44 100644
--- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
+++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
@@ -22,7 +22,7 @@
40
- 200
+ 220
36
36
32
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/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs
index 5353e8175d..4e47a720f0 100644
--- a/src/Avalonia.Base/Data/BindingOperations.cs
+++ b/src/Avalonia.Base/Data/BindingOperations.cs
@@ -99,14 +99,15 @@ namespace Avalonia.Data
private sealed class TwoWayBindingDisposable : IDisposable
{
- private readonly IDisposable _first;
- private readonly IDisposable _second;
+ private readonly IDisposable _toTargetSubscription;
+ private readonly IDisposable _fromTargetSubsription;
+
private bool _isDisposed;
- public TwoWayBindingDisposable(IDisposable first, IDisposable second)
+ public TwoWayBindingDisposable(IDisposable toTargetSubscription, IDisposable fromTargetSubsription)
{
- _first = first;
- _second = second;
+ _toTargetSubscription = toTargetSubscription;
+ _fromTargetSubsription = fromTargetSubsription;
}
public void Dispose()
@@ -116,8 +117,8 @@ namespace Avalonia.Data
return;
}
- _first.Dispose();
- _second.Dispose();
+ _fromTargetSubsription.Dispose();
+ _toTargetSubscription.Dispose();
_isDisposed = true;
}
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/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs
index 16ba571a5a..535a826c1e 100644
--- a/src/Avalonia.Base/StyledPropertyBase.cs
+++ b/src/Avalonia.Base/StyledPropertyBase.cs
@@ -159,9 +159,9 @@ namespace Avalonia
}
///
- public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data)
+ public override void Accept(IAvaloniaPropertyVisitor visitor, ref TData data)
{
- vistor.Visit(this, ref data);
+ visitor.Visit(this, ref data);
}
///
@@ -242,11 +242,5 @@ namespace Avalonia
_ = type ?? throw new ArgumentNullException(nameof(type));
return GetMetadata(type).DefaultValue;
}
-
- [DebuggerHidden]
- private Func Cast(Func validate)
- {
- return (o, v) => validate((THost)o, v);
- }
}
}
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 94ff92a2b0..7a876ac862 100644
--- a/src/Avalonia.Controls/Button.cs
+++ b/src/Avalonia.Controls/Button.cs
@@ -604,6 +604,7 @@ namespace Avalonia.Controls
if (e.Key == Key.Enter && IsVisible && IsEnabled)
{
OnClick();
+ e.Handled = true;
}
}
@@ -617,6 +618,7 @@ namespace Avalonia.Controls
if (e.Key == Key.Escape && IsVisible && IsEnabled)
{
OnClick();
+ e.Handled = true;
}
}
diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
index 5948d81810..a923c44d05 100644
--- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
+++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
@@ -2,6 +2,8 @@
using System.Globalization;
using System.Linq;
using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
@@ -58,18 +60,17 @@ namespace Avalonia.Controls.Primitives
private Vector _offset;
private bool _hasInit;
private bool _suppressUpdateOffset;
- private ListBoxItem? _pressedItem;
public DateTimePickerPanel()
{
FormatDate = DateTime.Now;
- AddHandler(ListBoxItem.PointerPressedEvent, OnItemPointerDown, Avalonia.Interactivity.RoutingStrategies.Bubble);
- AddHandler(ListBoxItem.PointerReleasedEvent, OnItemPointerUp, Avalonia.Interactivity.RoutingStrategies.Bubble);
+ AddHandler(TappedEvent, OnItemTapped, RoutingStrategies.Bubble);
}
static DateTimePickerPanel()
{
FocusableProperty.OverrideDefaultValue(true);
+ BackgroundProperty.OverrideDefaultValue(Brushes.Transparent);
AffectsMeasure(ItemHeightProperty);
}
@@ -523,26 +524,13 @@ namespace Avalonia.Controls.Primitives
return newValue;
}
- private void OnItemPointerDown(object? sender, PointerPressedEventArgs e)
+ private void OnItemTapped(object? sender, TappedEventArgs e)
{
- if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed &&
- e.Source is IVisual source)
- {
- _pressedItem = GetItemFromSource(source);
- e.Handled = true;
- }
- }
-
- private void OnItemPointerUp(object? sender, PointerReleasedEventArgs e)
- {
- if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonReleased &&
- _pressedItem != null &&
- e.Source is IVisual source &&
- GetItemFromSource(source) is ListBoxItem item &&
- item.Tag is int tag)
+ if (e.Source is IVisual source &&
+ GetItemFromSource(source) is ListBoxItem listBoxItem &&
+ listBoxItem.Tag is int tag)
{
SelectedValue = tag;
- _pressedItem = null;
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.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index 9886dd913a..93acd88fb1 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
@@ -107,9 +107,6 @@ namespace Avalonia.Controls.Presenters
AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
AffectsArrange(HorizontalContentAlignmentProperty, VerticalContentAlignmentProperty);
AffectsMeasure(BorderThicknessProperty, PaddingProperty);
- ContentProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e));
- ContentTemplateProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e));
- TemplatedParentProperty.Changed.AddClassHandler((x, e) => x.TemplatedParentChanged(e));
}
public ContentPresenter()
@@ -240,6 +237,21 @@ namespace Avalonia.Controls.Presenters
}
}
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+ switch (change.Property.Name)
+ {
+ case nameof(Content):
+ case nameof(ContentTemplate):
+ ContentChanged(change);
+ break;
+ case nameof(TemplatedParent):
+ TemplatedParentChanged(change);
+ break;
+ }
+ }
+
///
/// Updates the control based on the control's .
///
@@ -254,8 +266,14 @@ namespace Avalonia.Controls.Presenters
public void UpdateChild()
{
var content = Content;
+ UpdateChild(content);
+ }
+
+ private void UpdateChild(object? content)
+ {
+ var contentTemplate = ContentTemplate;
var oldChild = Child;
- var newChild = CreateChild();
+ var newChild = CreateChild(content, oldChild, contentTemplate);
var logicalChildren = Host?.LogicalChildren ?? LogicalChildren;
// Remove the old child if we're not recycling it.
@@ -271,7 +289,7 @@ namespace Avalonia.Controls.Presenters
}
// Set the DataContext if the data isn't a control.
- if (!(content is IControl))
+ if (contentTemplate is { } || !(content is IControl))
{
DataContext = content;
}
@@ -299,6 +317,7 @@ namespace Avalonia.Controls.Presenters
}
_createdChild = true;
+
}
///
@@ -325,18 +344,23 @@ namespace Avalonia.Controls.Presenters
{
var content = Content;
var oldChild = Child;
+ return CreateChild(content, oldChild, ContentTemplate);
+ }
+
+ private IControl? CreateChild(object? content, IControl? oldChild, IDataTemplate? template)
+ {
var newChild = content as IControl;
// We want to allow creating Child from the Template, if Content is null.
// But it's important to not use DataTemplates, otherwise we will break content presenters in many places,
// otherwise it will blow up every ContentPresenter without Content set.
- if (newChild == null
- && (content != null || ContentTemplate != null))
+ if ((newChild == null
+ && (content != null || template != null)) || (newChild is { } && template is { }))
{
- var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ??
+ var dataTemplate = this.FindDataTemplate(content, template) ??
(
- RecognizesAccessKey
- ? FuncDataTemplate.Access
+ RecognizesAccessKey
+ ? FuncDataTemplate.Access
: FuncDataTemplate.Default
);
@@ -446,7 +470,14 @@ namespace Avalonia.Controls.Presenters
if (((ILogical)this).IsAttachedToLogicalTree)
{
- UpdateChild();
+ if (e.Property.Name == nameof(Content))
+ {
+ UpdateChild(e.NewValue);
+ }
+ else
+ {
+ UpdateChild();
+ }
}
else if (Child != null)
{
diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs
index 6a0408d6d1..468879edd1 100644
--- a/src/Avalonia.Controls/Primitives/OverlayLayer.cs
+++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs
@@ -23,7 +23,14 @@ namespace Avalonia.Controls.Primitives
}
public bool HitTest(Point point) => Children.HitTestCustom(point);
-
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ foreach (Control child in Children)
+ child.Measure(availableSize);
+ return availableSize;
+ }
+
protected override Size ArrangeOverride(Size finalSize)
{
// We are saving it here since child controls might need to know the entire size of the overlay
diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs
index 017a053c48..1d8d6aef45 100644
--- a/src/Avalonia.Controls/ProgressBar.cs
+++ b/src/Avalonia.Controls/ProgressBar.cs
@@ -1,6 +1,7 @@
using System;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
+using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Media;
@@ -137,6 +138,7 @@ namespace Avalonia.Controls
static ProgressBar()
{
+ ValueProperty.OverrideMetadata(new DirectPropertyMetadata(defaultBindingMode: BindingMode.OneWay));
ValueProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e));
MinimumProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e));
MaximumProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e));
diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
index 46ee8e686c..5672641602 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
@@ -41,5 +41,10 @@ namespace Avalonia.Diagnostics
/// Default handler is
public IScreenshotHandler ScreenshotHandler { get; set; }
= Convetions.DefaultScreenshotHandler;
+
+ ///
+ /// Gets or sets whether DevTools should use the dark mode theme
+ ///
+ public bool UseDarkMode { get; set; }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs
new file mode 100644
index 0000000000..16973a96ef
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs
@@ -0,0 +1,56 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Media;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class BindingSetterViewModel : SetterViewModel
+ {
+ public BindingSetterViewModel(AvaloniaProperty property, object? value) : base(property, value)
+ {
+ switch (value)
+ {
+ case Binding binding:
+ Path = binding.Path;
+ Tint = Brushes.CornflowerBlue;
+ ValueTypeTooltip = "Reflection Binding";
+
+ break;
+ case CompiledBindingExtension binding:
+ Path = binding.Path.ToString();
+ Tint = Brushes.DarkGreen;
+ ValueTypeTooltip = "Compiled Binding";
+
+ break;
+ case TemplateBinding binding:
+ if (binding.Property is AvaloniaProperty templateProperty)
+ {
+ Path = $"{templateProperty.OwnerType.Name}.{templateProperty.Name}";
+ }
+ else
+ {
+ Path = "Unassigned";
+ }
+
+ Tint = Brushes.OrangeRed;
+ ValueTypeTooltip = "Template Binding";
+
+ break;
+ default:
+ throw new ArgumentException("Invalid binding type", nameof(value));
+ }
+ }
+
+ public IBrush Tint { get; }
+
+ public string ValueTypeTooltip { get; }
+
+ public string Path { get; }
+
+ public override void CopyValue()
+ {
+ CopyToClipboard(Path);
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
index 8f0a4d07b0..a1fd8ed028 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
@@ -8,6 +8,7 @@ using System.Reflection;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
+using Avalonia.Data;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Styling;
using Avalonia.VisualTree;
@@ -87,7 +88,16 @@ namespace Avalonia.Diagnostics.ViewModels
}
else
{
- setterVm = new SetterViewModel(regularSetter.Property, setterValue);
+ var isBinding = IsBinding(setterValue);
+
+ if (isBinding)
+ {
+ setterVm = new BindingSetterViewModel(regularSetter.Property, setterValue);
+ }
+ else
+ {
+ setterVm = new SetterViewModel(regularSetter.Property, setterValue);
+ }
}
setters.Add(setterVm);
@@ -117,6 +127,19 @@ namespace Avalonia.Diagnostics.ViewModels
return null;
}
+ private bool IsBinding(object? value)
+ {
+ switch (value)
+ {
+ case Binding:
+ case CompiledBindingExtension:
+ case TemplateBinding:
+ return true;
+ }
+
+ return false;
+ }
+
public TreePageViewModel TreePage { get; }
public DataGridCollectionView? PropertiesView
@@ -132,31 +155,19 @@ namespace Avalonia.Diagnostics.ViewModels
public object? SelectedEntity
{
get => _selectedEntity;
- set
- {
- RaiseAndSetIfChanged(ref _selectedEntity, value);
-
- }
+ set => RaiseAndSetIfChanged(ref _selectedEntity, value);
}
public string? SelectedEntityName
{
get => _selectedEntityName;
- set
- {
- RaiseAndSetIfChanged(ref _selectedEntityName, value);
-
- }
+ set => RaiseAndSetIfChanged(ref _selectedEntityName, value);
}
public string? SelectedEntityType
{
get => _selectedEntityType;
- set
- {
- RaiseAndSetIfChanged(ref _selectedEntityType, value);
-
- }
+ set => RaiseAndSetIfChanged(ref _selectedEntityType, value);
}
public PropertyViewModel? SelectedProperty
@@ -479,6 +490,31 @@ namespace Avalonia.Diagnostics.ViewModels
inpc2.PropertyChanged += ControlPropertyChanged;
}
}
+
+ internal void SelectProperty(AvaloniaProperty property)
+ {
+ SelectedProperty = null;
+
+ if (SelectedEntity != _avaloniaObject)
+ {
+ NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString());
+ }
+
+ if (PropertiesView is null)
+ {
+ return;
+ }
+
+ foreach (object o in PropertiesView)
+ {
+ if (o is AvaloniaPropertyViewModel propertyVm && propertyVm.Property == property)
+ {
+ SelectedProperty = propertyVm;
+
+ break;
+ }
+ }
+ }
internal void UpdatePropertiesView(bool showImplementedInterfaces)
{
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
index e93dc7361b..5202ac963e 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
@@ -7,11 +7,14 @@ namespace Avalonia.Diagnostics.ViewModels
public object Key { get; }
public IBrush Tint { get; }
+
+ public string ValueTypeTooltip { get; }
public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object? resourceValue, bool isDynamic) : base(property, resourceValue)
{
Key = resourceKey;
Tint = isDynamic ? Brushes.Orange : Brushes.Brown;
+ ValueTypeTooltip = isDynamic ? "Dynamic Resource" : "Static Resource";
}
public void CopyResourceKey()
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
index 38cbefcb93..559ed49911 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
@@ -34,7 +34,7 @@ namespace Avalonia.Diagnostics.ViewModels
IsVisible = true;
}
- public void CopyValue()
+ public virtual void CopyValue()
{
var textToCopy = Value?.ToString();
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
index d7acbbd577..cc392853be 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
@@ -44,7 +44,9 @@
UseWholeWordFilter="{Binding UseWholeWordFilter}"
UseRegexFilter="{Binding UseRegexFilter}"/>
-
+
+
+
+
+
+
+
+
+
+
@@ -148,6 +170,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+
+
+ }
+
+
+
+
@@ -159,15 +201,15 @@
-
+
(
-
+
)
-
+
@@ -180,11 +222,11 @@
-
+
-
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
index 78919a2105..08ffe2c081 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
@@ -7,9 +7,13 @@ namespace Avalonia.Diagnostics.Views
{
internal class ControlDetailsView : UserControl
{
+ private DataGrid _dataGrid;
+
public ControlDetailsView()
{
InitializeComponent();
+
+ _dataGrid = this.GetControl("DataGrid");
}
private void InitializeComponent()
@@ -25,5 +29,25 @@ namespace Avalonia.Diagnostics.Views
}
}
+
+ private void PropertyNamePressed(object sender, PointerPressedEventArgs e)
+ {
+ var mainVm = (ControlDetailsViewModel?) DataContext;
+
+ if (mainVm is null)
+ {
+ return;
+ }
+
+ if (sender is Control control && control.DataContext is SetterViewModel setterVm)
+ {
+ mainVm.SelectProperty(setterVm.Property);
+
+ if (mainVm.SelectedProperty is not null)
+ {
+ _dataGrid.ScrollIntoView(mainVm.SelectedProperty, null);
+ }
+ }
+ }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml
index af6c84a76a..db48aaaa8e 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml
@@ -15,6 +15,7 @@
+
@@ -46,6 +47,7 @@
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
index a3cff7f3d3..1e152dcdd4 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
@@ -11,6 +11,7 @@ using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
+using Avalonia.Themes.Default;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.Views
@@ -242,7 +243,17 @@ namespace Avalonia.Diagnostics.Views
private void RootClosed(object? sender, EventArgs e) => Close();
- public void SetOptions(DevToolsOptions options) =>
+ public void SetOptions(DevToolsOptions options)
+ {
(DataContext as MainViewModel)?.SetOptions(options);
+
+ if (options.UseDarkMode)
+ {
+ if (Styles[0] is SimpleTheme st)
+ {
+ st.Mode = SimpleThemeMode.Dark;
+ }
+ }
+ }
}
}
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.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs
index 0060767565..30e8661e89 100644
--- a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs
+++ b/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs
@@ -5,14 +5,27 @@ using System.Collections.Generic;
namespace Avalonia.Controls.Metadata
{
+ ///
+ /// Defines all pseudoclasses by name referenced and implemented by a control.
+ ///
+ ///
+ /// This is currently used for code-completion in certain IDEs.
+ ///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class PseudoClassesAttribute : Attribute
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The list of pseudoclass names.
public PseudoClassesAttribute(params string[] pseudoClasses)
{
PseudoClasses = pseudoClasses;
}
+ ///
+ /// Gets the list of pseudoclass names.
+ ///
public IReadOnlyList PseudoClasses { get; }
}
}
diff --git a/src/Avalonia.Styling/Controls/Metadata/TemplatePartAttribute.cs b/src/Avalonia.Styling/Controls/Metadata/TemplatePartAttribute.cs
new file mode 100644
index 0000000000..3b8f971713
--- /dev/null
+++ b/src/Avalonia.Styling/Controls/Metadata/TemplatePartAttribute.cs
@@ -0,0 +1,55 @@
+// This source file is adapted from the Windows Presentation Foundation project.
+// (https://github.com/dotnet/wpf/)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+
+#nullable enable
+
+namespace Avalonia.Controls.Metadata
+{
+ ///
+ /// Defines a control template part referenced by name in code.
+ /// Template part names should begin with the "PART_" prefix.
+ ///
+ ///
+ /// Style authors should be able to identify the part type used for styling the specific control.
+ /// The part is usually required in the style and should have a specific predefined name.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+ public sealed class TemplatePartAttribute : Attribute
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TemplatePartAttribute()
+ {
+ Name = string.Empty;
+ Type = typeof(object);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The part name used by the class to identify a required element in the style.
+ /// The type of the element that should be used as a part with name.
+ public TemplatePartAttribute(string name, Type type)
+ {
+ Name = name;
+ Type = type;
+ }
+
+ ///
+ /// Gets or sets the part name used by the class to identify a required element in the style.
+ /// Template part names should begin with the "PART_" prefix.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the type of the element that should be used as a part with name specified
+ /// in .
+ ///
+ public Type Type { get; set; }
+ }
+}
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 @@