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/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/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/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/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/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/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/ViewModels/BindingSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs
new file mode 100644
index 0000000000..de3e56e514
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs
@@ -0,0 +1,51 @@
+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;
+
+ break;
+ case CompiledBindingExtension binding:
+ Path = binding.Path.ToString();
+ Tint = Brushes.DarkGreen;
+
+ break;
+ case TemplateBinding binding:
+ if (binding.Property is AvaloniaProperty templateProperty)
+ {
+ Path = $"{templateProperty.OwnerType.Name}.{templateProperty.Name}";
+ }
+ else
+ {
+ Path = "Unassigned";
+ }
+
+ Tint = Brushes.OrangeRed;
+
+ break;
+ default:
+ throw new ArgumentException("Invalid binding type", nameof(value));
+ }
+ }
+
+ public IBrush Tint { 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..701947d2ab 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
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..ec97b213a9 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
@@ -138,6 +138,26 @@
+
+
+
+
+
+
+
+
+
+
+
+ {
+
+
+ }
+
+
+
+
+
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 @@
-
@@ -195,8 +188,8 @@
-
@@ -213,8 +206,8 @@
-
@@ -226,8 +219,8 @@
-
diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
index c82757799d..095a2ef51f 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
@@ -1,4 +1,6 @@
-
+
+ IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}" />
diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml
index a9e6569158..a2b50a859d 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml
@@ -220,7 +220,7 @@