Browse Source

Merge branch 'master' into fixes/make-androidtest-buildable

pull/7811/head
Max Katz 5 years ago
committed by GitHub
parent
commit
e781b2df20
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 27
      Avalonia.sln
  2. 18
      samples/ControlCatalog/Converter/MathSubtractConverter.cs
  3. 3
      samples/ControlCatalog/MainView.xaml
  4. 3
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  5. 82
      samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
  6. 19
      samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs
  7. 309
      samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
  8. 2
      samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
  9. 15
      src/Avalonia.Base/Data/BindingOperations.cs
  10. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  11. 70
      src/Avalonia.Base/Utilities/UriExtensions.cs
  12. 2
      src/Avalonia.Controls/Button.cs
  13. 28
      src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
  14. 53
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  15. 9
      src/Avalonia.Controls/Primitives/OverlayLayer.cs
  16. 2
      src/Avalonia.Controls/ProgressBar.cs
  17. 51
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/BindingSetterViewModel.cs
  18. 25
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  19. 2
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
  20. 20
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  21. 241
      src/Avalonia.PlatformSupport/AssetLoader.cs
  22. 5
      src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj
  23. 42
      src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs
  24. 42
      src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs
  25. 52
      src/Avalonia.PlatformSupport/Internal/AssetDescriptor.cs
  26. 6
      src/Avalonia.PlatformSupport/Internal/Constants.cs
  27. 59
      src/Avalonia.PlatformSupport/Internal/SlicedStream.cs
  28. 2
      src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml
  29. 43
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  30. 6
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  31. 2
      src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml
  32. 150
      src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs
  33. 10
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs
  34. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  35. 29
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  36. 29
      tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs
  37. 23
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
  38. 31
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
  39. 31
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
  40. 62
      tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs
  41. 24
      tests/Avalonia.PlatformSupport.UnitTests/Avalonia.PlatformSupport.UnitTests.csproj
  42. 5
      tests/Avalonia.UnitTests/MockAssetLoader.cs
  43. 45
      tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs

27
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}

18
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();
}
}

3
samples/ControlCatalog/MainView.xaml

@ -145,6 +145,9 @@
<TabItem Header="ToolTip">
<pages:ToolTipPage />
</TabItem>
<TabItem Header="TransitioningContentControl">
<pages:TransitioningContentControlPage />
</TabItem>
<TabItem Header="TreeView">
<pages:TreeViewPage />
</TabItem>

3
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
})
}
},

82
samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml

@ -0,0 +1,82 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:ControlCatalog.ViewModels"
xmlns:converter="clr-namespace:ControlCatalog.Converter"
xmlns:system="using:System"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:TransitioningContentControlPageViewModel"
x:CompileBindings="True"
x:Class="ControlCatalog.Pages.TransitioningContentControlPage">
<UserControl.DataContext>
<vm:TransitioningContentControlPageViewModel />
</UserControl.DataContext>
<UserControl.Styles>
<Style Selector="HeaderedContentControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="HeaderCol" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{TemplateBinding Header}"
Grid.Column="0"
VerticalAlignment="Center" />
<ContentPresenter Content="{TemplateBinding Content}"
Grid.Column="1"
VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Styles>
<UserControl.Resources>
<converter:MathSubtractConverter x:Key="MathSubtractConverter" />
<system:Double x:Key="TopMargin">8</system:Double>
</UserControl.Resources>
<DockPanel LastChildFill="True"
Height="{Binding Path=Bounds.Height, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Grid}, Converter={StaticResource MathSubtractConverter},ConverterParameter={StaticResource TopMargin}}">
<TextBlock DockPanel.Dock="Top" Classes="h2">The TransitioningContentControl control allows you to show a page transition whenever the Content changes.</TextBlock>
<ExperimentalAcrylicBorder DockPanel.Dock="Bottom" Margin="10" CornerRadius="5" >
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial BackgroundSource="Digger" TintColor="White" />
</ExperimentalAcrylicBorder.Material>
<StackPanel Margin="5" Spacing="5" Grid.IsSharedSizeScope="True">
<HeaderedContentControl Header="Select a transition">
<ComboBox Items="{Binding PageTransitions}" SelectedItem="{Binding SelectedTransition}" />
</HeaderedContentControl>
<HeaderedContentControl Header="Duration">
<NumericUpDown Value="{Binding Duration}" Increment="250" Minimum="100" />
</HeaderedContentControl>
<HeaderedContentControl Header="Clip to Bounds">
<ToggleSwitch IsChecked="{Binding ClipToBounds}" />
</HeaderedContentControl>
</StackPanel>
</ExperimentalAcrylicBorder>
<Button DockPanel.Dock="Left" Command="{Binding PrevImage}" Content="&lt;" />
<Button DockPanel.Dock="Right" Command="{Binding NextImage}" Content=">" />
<Border ClipToBounds="{Binding ClipToBounds}" Margin="5">
<TransitioningContentControl Content="{Binding SelectedImage}"
PageTransition="{Binding SelectedTransition.Transition}" >
<TransitioningContentControl.ContentTemplate>
<DataTemplate DataType="Bitmap">
<Image Source="{Binding}" />
</DataTemplate>
</TransitioningContentControl.ContentTemplate>
</TransitioningContentControl>
</Border>
</DockPanel>
</UserControl>

19
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);
}
}
}

309
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<IAssetLoader>();
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<PageTransition> PageTransitions { get; } = new List<PageTransition>();
public List<Bitmap> Images { get; } = new List<Bitmap>();
private Bitmap? _SelectedImage;
/// <summary>
/// Gets or Sets the selected image
/// </summary>
public Bitmap? SelectedImage
{
get { return _SelectedImage; }
set { this.RaiseAndSetIfChanged(ref _SelectedImage, value); }
}
private PageTransition _SelectedTransition;
/// <summary>
/// Gets or sets the transition to play
/// </summary>
public PageTransition SelectedTransition
{
get { return _SelectedTransition; }
set { this.RaiseAndSetIfChanged(ref _SelectedTransition, value); }
}
private bool _ClipToBounds;
/// <summary>
/// Gets or sets if the content should be clipped to bounds
/// </summary>
public bool ClipToBounds
{
get { return _ClipToBounds; }
set { this.RaiseAndSetIfChanged(ref _ClipToBounds, value); }
}
private int _Duration = 500;
/// <summary>
/// Gets or Sets the duration
/// </summary>
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;
/// <summary>
/// Gets or sets the transition
/// </summary>
public IPageTransition Transition
{
get { return _Transition; }
set { this.RaiseAndSetIfChanged(ref _Transition, value); }
}
public override string ToString()
{
return DisplayTitle;
}
}
public class CustomTransition : IPageTransition
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomTransition"/> class.
/// </summary>
public CustomTransition()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomTransition"/> class.
/// </summary>
/// <param name="duration">The duration of the animation.</param>
public CustomTransition(TimeSpan duration)
{
Duration = duration;
}
/// <summary>
/// Gets the duration of the animation.
/// </summary>
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<Task>();
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;
}
}
/// <summary>
/// Gets the common visual parent of the two control.
/// </summary>
/// <param name="from">The from control.</param>
/// <param name="to">The to control.</param>
/// <returns>The common parent.</returns>
/// <exception cref="ArgumentException">
/// The two controls do not share a common parent.
/// </exception>
/// <remarks>
/// Any one of the parameters may be null, but not both.
/// </remarks>
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.");
}
}
}

2
samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml

@ -22,7 +22,7 @@
<Styles.Resources>
<x:Double x:Key="PaneCompactWidth">40</x:Double>
<x:Double x:Key="PaneExpandWidth">200</x:Double>
<x:Double x:Key="PaneExpandWidth">220</x:Double>
<x:Double x:Key="HeaderHeight">36</x:Double>
<x:Double x:Key="NavigationItemHeight">36</x:Double>
<x:Double x:Key="HamburgerMenuButtonHeight">32</x:Double>

15
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;
}

1
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")]

70
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 "";
}
}

2
src/Avalonia.Controls/Button.cs

@ -539,6 +539,7 @@ namespace Avalonia.Controls
if (e.Key == Key.Enter && IsVisible && IsEnabled)
{
OnClick();
e.Handled = true;
}
}
@ -552,6 +553,7 @@ namespace Avalonia.Controls
if (e.Key == Key.Escape && IsVisible && IsEnabled)
{
OnClick();
e.Handled = true;
}
}

28
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<DateTimePickerPanel>(true);
BackgroundProperty.OverrideDefaultValue<DateTimePickerPanel>(Brushes.Transparent);
AffectsMeasure<DateTimePickerPanel>(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;
}
}

53
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -107,9 +107,6 @@ namespace Avalonia.Controls.Presenters
AffectsRender<ContentPresenter>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
AffectsArrange<ContentPresenter>(HorizontalContentAlignmentProperty, VerticalContentAlignmentProperty);
AffectsMeasure<ContentPresenter>(BorderThicknessProperty, PaddingProperty);
ContentProperty.Changed.AddClassHandler<ContentPresenter>((x, e) => x.ContentChanged(e));
ContentTemplateProperty.Changed.AddClassHandler<ContentPresenter>((x, e) => x.ContentChanged(e));
TemplatedParentProperty.Changed.AddClassHandler<ContentPresenter>((x, e) => x.TemplatedParentChanged(e));
}
public ContentPresenter()
@ -240,6 +237,21 @@ namespace Avalonia.Controls.Presenters
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(Content):
case nameof(ContentTemplate):
ContentChanged(change);
break;
case nameof(TemplatedParent):
TemplatedParentChanged(change);
break;
}
}
/// <summary>
/// Updates the <see cref="Child"/> control based on the control's <see cref="Content"/>.
/// </summary>
@ -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;
}
/// <inheritdoc/>
@ -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)
{

9
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

2
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<ProgressBar>(new DirectPropertyMetadata<double>(defaultBindingMode: BindingMode.OneWay));
ValueProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
MinimumProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
MaximumProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));

51
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);
}
}
}

25
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

2
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();

20
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -138,6 +138,26 @@
</StackPanel>
</DataTemplate>
<DataTemplate DataType="vm:BindingSetterViewModel">
<Panel Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}" IsVisible="{Binding IsVisible}" HorizontalAlignment="Left">
<Panel.ContextMenu>
<ContextMenu>
<MenuItem Header="Copy property name" Command="{Binding CopyPropertyName} "/>
<MenuItem Header="Copy value" Command="{Binding CopyValue} "/>
</ContextMenu>
</Panel.ContextMenu>
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" />
<TextBlock Text=":" />
<TextBlock>{</TextBlock>
<Rectangle Height="8" Width="8" VerticalAlignment="Center" Fill="{Binding Tint}"/>
<TextBlock Text="{Binding Path}" />
<TextBlock>}</TextBlock>
</StackPanel>
<Rectangle Height="1" Fill="#6C6C6C" VerticalAlignment="Center" IsVisible="{Binding !IsActive}" />
</Panel>
</DataTemplate>
<DataTemplate DataType="Color">
<StackPanel Orientation="Horizontal" Spacing="2">
<Border BorderThickness="1" BorderBrush="Black" Width="8" Height="8">

241
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
/// </summary>
public class AssetLoader : IAssetLoader
{
private const string AvaloniaResourceName = "!AvaloniaResources";
private static readonly Dictionary<string, AssemblyDescriptor> AssemblyNameCache
= new Dictionary<string, AssemblyDescriptor>();
private static AssemblyDescriptorResolver s_assemblyDescriptorResolver = new();
private AssemblyDescriptor? _defaultResmAssembly;
/// <remarks>
/// Introduced for tests.
/// </remarks>
internal static void SetAssemblyDescriptorResolver(AssemblyDescriptorResolver resolver) =>
s_assemblyDescriptorResolver = resolver;
/// <summary>
/// Initializes a new instance of the <see cref="AssetLoader"/> class.
/// </summary>
@ -109,17 +114,18 @@ namespace Avalonia.PlatformSupport
/// <returns>All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset</returns>
public IEnumerable<Uri> 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>();
}
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<Uri>();
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<Uri>();
}
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<string, string> 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<string, IAssetDescriptor>? Resources { get; }
public Dictionary<string, IAssetDescriptor>? AvaloniaResources { get; }
public string? Name { get; }
}
public static void RegisterResUriParsers()
{
if (!UriParser.IsKnownScheme("avares"))

5
src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj

@ -16,4 +16,9 @@
<Import Project="..\..\build\NetCore.props" />
<Import Project="..\..\build\NetFX.props" />
<Import Project="..\..\build\NullableEnable.props" />
<ItemGroup>
<InternalsVisibleTo Include="$(AssemblyName).UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87" />
</ItemGroup>
</Project>

42
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<string, IAssetDescriptor>? Resources { get; }
public Dictionary<string, IAssetDescriptor>? AvaloniaResources { get; }
public string? Name { get; }
private static string GetPathRooted(AvaloniaResourcesIndexEntry r) =>
r.Path![0] == '/' ? r.Path : '/' + r.Path;
}

42
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<string, AssemblyDescriptor> _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;
}
}

52
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);
}
}

6
src/Avalonia.PlatformSupport/Internal/Constants.cs

@ -0,0 +1,6 @@
namespace Avalonia.PlatformSupport.Internal;
internal static class Constants
{
public static string AvaloniaResourceName => "!AvaloniaResources";
}

59
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();
}

2
src/Avalonia.Themes.Default/Controls/ToggleSwitch.xaml

@ -254,7 +254,7 @@
<Style Selector="ToggleSwitch:disabled /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffDisabled}"/>
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffPressed}"/>
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffDisabled}"/>
</Style>
<Style Selector="ToggleSwitch:disabled /template/ Ellipse#SwitchKnobOff">

43
src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

@ -101,23 +101,17 @@
IsVisible="False"
HorizontalAlignment="Right" />
<Viewbox UseLayoutRounding="False"
MinHeight="{DynamicResource ComboBoxMinHeight}"
Grid.Row="1"
Grid.Column="1"
IsHitTestVisible="False"
Margin="0,0,10,0"
Height="12"
Width="12"
HorizontalAlignment="Right"
VerticalAlignment="Center">
<Panel>
<Panel Height="12"
Width="12" />
<Path x:Name="DropDownGlyph"
<PathIcon x:Name="DropDownGlyph"
Grid.Row="1"
Grid.Column="1"
UseLayoutRounding="False"
IsHitTestVisible="False"
Height="12"
Width="12"
Margin="0,0,10,0"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
</Panel>
</Viewbox>
<Popup Name="PART_Popup"
WindowManagerAddShadowHint="False"
IsOpen="{TemplateBinding IsDropDownOpen, Mode=TwoWay}"
@ -159,10 +153,9 @@
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="ComboBox /template/ Path#DropDownGlyph">
<Setter Property="Fill" Value="{DynamicResource ComboBoxDropDownGlyphForeground}" />
<Style Selector="ComboBox /template/ PathIcon#DropDownGlyph">
<Setter Property="Foreground" Value="{DynamicResource ComboBoxDropDownGlyphForeground}" />
<Setter Property="Data" Value="M1939 486L2029 576L1024 1581L19 576L109 486L1024 1401L1939 486Z" />
<Setter Property="Stretch" Value="Uniform" />
</Style>
<!-- PointerOver State -->
@ -195,8 +188,8 @@
<Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundDisabled}" />
</Style>
<Style Selector="ComboBox:disabled /template/ Path#DropDownGlyph">
<Setter Property="Fill" Value="{DynamicResource ComboBoxDropDownGlyphForegroundDisabled}" />
<Style Selector="ComboBox:disabled /template/ PathIcon#DropDownGlyph">
<Setter Property="Foreground" Value="{DynamicResource ComboBoxDropDownGlyphForegroundDisabled}" />
</Style>
<!-- Focused State -->
@ -213,8 +206,8 @@
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ComboBoxForegroundFocused}" />
</Style>
<Style Selector="ComboBox:focus-visible /template/ Path#DropDownGlyph">
<Setter Property="Fill" Value="{DynamicResource ComboBoxDropDownGlyphForegroundFocused}" />
<Style Selector="ComboBox:focus-visible /template/ PathIcon#DropDownGlyph">
<Setter Property="Foreground" Value="{DynamicResource ComboBoxDropDownGlyphForegroundFocused}" />
</Style>
<!-- Focus Pressed State -->
@ -226,8 +219,8 @@
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ComboBoxPlaceHolderForegroundFocusedPressed}" />
</Style>
<Style Selector="ComboBox:focused:pressed /template/ Path#DropDownGlyph">
<Setter Property="Fill" Value="{DynamicResource ComboBoxDropDownGlyphForegroundFocusedPressed}" />
<Style Selector="ComboBox:focused:pressed /template/ PathIcon#DropDownGlyph">
<Setter Property="Foreground" Value="{DynamicResource ComboBoxDropDownGlyphForegroundFocusedPressed}" />
</Style>
<!-- Error State -->

6
src/Avalonia.Themes.Fluent/Controls/TextBox.xaml

@ -1,4 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True">
<Design.PreviewWith>
<Border Padding="20">
<TextBox Text="Sample"
@ -168,7 +170,7 @@
<Setter Property="InnerRightContent">
<Template>
<ToggleButton Classes="passwordBoxRevealButton"
IsChecked="{CompiledBinding $parent[TextBox].RevealPassword, Mode=TwoWay}" />
IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}" />
</Template>
</Setter>
</Style>

2
src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml

@ -220,7 +220,7 @@
<Style Selector="ToggleSwitch:disabled /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffDisabled}"/>
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffPressed}"/>
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffDisabled}"/>
</Style>
<Style Selector="ToggleSwitch:disabled /template/ Ellipse#SwitchKnobOff">

150
src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media.Fonts
{
@ -12,18 +13,10 @@ namespace Avalonia.Media.Fonts
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <returns></returns>
public static IEnumerable<Uri> LoadFontAssets(FontFamilyKey fontFamilyKey)
{
var sourceWithoutArguments = fontFamilyKey.Source.OriginalString.Split('?').First();
if (sourceWithoutArguments.EndsWith(".ttf")
|| sourceWithoutArguments.EndsWith(".otf"))
{
return GetFontAssetsByExpression(fontFamilyKey);
}
return GetFontAssetsBySource(fontFamilyKey);
}
public static IEnumerable<Uri> LoadFontAssets(FontFamilyKey fontFamilyKey) =>
IsFontTtfOrOtf(fontFamilyKey.Source) ?
GetFontAssetsByExpression(fontFamilyKey) :
GetFontAssetsBySource(fontFamilyKey);
/// <summary>
/// Searches for font assets at a given location and returns a quantity of found assets
@ -34,11 +27,7 @@ namespace Avalonia.Media.Fonts
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
var matchingAssets =
availableAssets.Where(x => x.AbsolutePath.EndsWith(".ttf") || x.AbsolutePath.EndsWith(".otf"));
return matchingAssets;
return availableAssets.Where(x => IsFontTtfOrOtf(x));
}
/// <summary>
@ -49,71 +38,124 @@ namespace Avalonia.Media.Fonts
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
{
var (fileNameWithoutExtension, extension) = GetFileName(fontFamilyKey, out var location);
var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
var fileName = GetFileName(fontFamilyKey, out var fileExtension, out var location);
return availableResources.Where(x => IsContainsFile(x, filePattern, extension));
}
var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
private static (string fileNameWithoutExtension, string extension) GetFileName(
FontFamilyKey fontFamilyKey, out Uri location)
{
if (fontFamilyKey.Source.IsAbsoluteResm())
{
var fileName = GetFileNameAndExtension(fontFamilyKey.Source.GetUnescapeAbsolutePath(), '.');
var uriLocation = fontFamilyKey.Source.GetUnescapeAbsoluteUri()
.Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty);
location = new Uri(uriLocation, UriKind.RelativeOrAbsolute);
return fileName;
}
string compareTo;
var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString);
var fullFilename = filename.fileNameWithoutExtension + filename.extension;
if (fontFamilyKey.Source.IsAbsoluteUri)
if (fontFamilyKey.BaseUri != null)
{
if (fontFamilyKey.Source.Scheme == "resm")
{
compareTo = location.AbsolutePath + "." + fileName.Split('*').First();
}
else
{
compareTo = location.AbsolutePath + fileName.Split('*').First();
}
var relativePath = fontFamilyKey.Source.OriginalString
.Replace(fullFilename, string.Empty);
location = new Uri(fontFamilyKey.BaseUri, relativePath);
}
else
{
compareTo = location.AbsolutePath + fileName.Split('*').First();
var uriString = fontFamilyKey.Source
.GetUnescapeAbsoluteUri()
.Replace(fullFilename, string.Empty);
location = new Uri(uriString);
}
var matchingResources = availableResources.Where(
x => x.AbsolutePath.Contains(compareTo)
&& x.AbsolutePath.EndsWith(fileExtension));
return filename;
}
return matchingResources;
private static string CreateFilePattern(
FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension)
{
var path = location.GetUnescapeAbsolutePath();
var file = GetSubString(fileNameWithoutExtension, '*');
return fontFamilyKey.Source.IsAbsoluteResm()
? path + "." + file
: path + file;
}
private static string GetFileName(FontFamilyKey fontFamilyKey, out string fileExtension, out Uri location)
private static bool IsContainsFile(Uri x, string filePattern, string fileExtension)
{
if (fontFamilyKey.Source.IsAbsoluteUri && fontFamilyKey.Source.Scheme == "resm")
{
fileExtension = "." + fontFamilyKey.Source.AbsolutePath.Split('.').LastOrDefault();
var path = x.GetUnescapeAbsolutePath();
return path.IndexOf(filePattern, StringComparison.Ordinal) >= 0
&& path.EndsWith(fileExtension, StringComparison.Ordinal);
}
var fileName = fontFamilyKey.Source.LocalPath.Replace(fileExtension, string.Empty).Split('.').Last();
private static bool IsFontTtfOrOtf(Uri uri)
{
var sourceWithoutArguments = GetSubString(uri.OriginalString, '?');
return sourceWithoutArguments.EndsWith(".ttf", StringComparison.Ordinal)
|| sourceWithoutArguments.EndsWith(".otf", StringComparison.Ordinal);
}
location = new Uri(fontFamilyKey.Source.AbsoluteUri.Replace("." + fileName + fileExtension, string.Empty), UriKind.RelativeOrAbsolute);
private static (string fileNameWithoutExtension, string extension) GetFileNameAndExtension(
string path, char directorySeparator = '/')
{
var pathAsSpan = path.AsSpan();
pathAsSpan = IsPathRooted(pathAsSpan, directorySeparator) ?
pathAsSpan.Slice(1, path.Length - 1) :
pathAsSpan;
return fileName;
}
var extension = GetFileExtension(pathAsSpan);
if (extension.Length == pathAsSpan.Length)
return (extension.ToString(), string.Empty);
var pathSegments = fontFamilyKey.Source.OriginalString.Split('/');
var fileName = GetFileName(pathAsSpan, directorySeparator, extension.Length);
return (fileName.ToString(), extension.ToString());
}
var fileNameWithExtension = pathSegments.Last();
private static bool IsPathRooted(ReadOnlySpan<char> path, char directorySeparator) =>
path.Length > 0 && path[0] == directorySeparator;
var fileNameSegments = fileNameWithExtension.Split('.');
private static ReadOnlySpan<char> GetFileExtension(ReadOnlySpan<char> path)
{
for (var i = path.Length - 1; i > 0; --i)
{
if (path[i] == '.')
return path.Slice(i, path.Length - i);
}
fileExtension = "." + fileNameSegments.Last();
return path;
}
if (fontFamilyKey.BaseUri != null)
private static ReadOnlySpan<char> GetFileName(ReadOnlySpan<char> path, char directorySeparator, int extensionLength)
{
for (var i = path.Length - extensionLength - 1; i >= 0; --i)
{
var relativePath = fontFamilyKey.Source.OriginalString
.Replace(fileNameWithExtension, string.Empty);
location = new Uri(fontFamilyKey.BaseUri, relativePath);
if (path[i] == directorySeparator)
return path.Slice(i + 1, path.Length - i - extensionLength - 1);
}
else
return path.Slice(0, path.Length - extensionLength);
}
private static string GetSubString(string path, char separator)
{
for (var i = 0; i < path.Length; i++)
{
location = new Uri(fontFamilyKey.Source.AbsolutePath.Replace(fileNameWithExtension, string.Empty));
if (path[i] == separator)
return path.Substring(0, i);
}
return fileNameSegments.First();
return path;
}
}
}

10
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs

@ -117,7 +117,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
return parentDataContextNode.DataContextType;
};
XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startTypeResolver, context.ParentNodes().OfType<XamlAstConstructableObjectNode>().First().Type.GetClrType());
var selfType = context.ParentNodes().OfType<XamlAstConstructableObjectNode>().First().Type.GetClrType();
// When using self bindings with setters we need to change target type to resolved selector type.
if (context.GetAvaloniaTypes().ISetter.IsAssignableFrom(selfType))
{
selfType = context.ParentNodes().OfType<AvaloniaXamlIlTargetTypeMetadataNode>().First().TargetType.GetClrType();
}
XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startTypeResolver, selfType);
}
return node;

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -91,6 +91,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType TextDecorationCollection { get; }
public IXamlType TextDecorations { get; }
public IXamlType TextTrimming { get; }
public IXamlType ISetter { get; }
public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{
@ -199,6 +200,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
TextDecorationCollection = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorationCollection");
TextDecorations = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorations");
TextTrimming = cfg.TypeSystem.GetType("Avalonia.Media.TextTrimming");
ISetter = cfg.TypeSystem.GetType("Avalonia.Styling.ISetter");
}
}

29
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -829,6 +829,30 @@ namespace Avalonia.Base.UnitTests
subscription.Dispose();
}
[Fact]
public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_With_Value()
{
var target = new Class1();
var source = new TestTwoWayBindingViewModel() { Value = 1 };
source.ResetSetterCalled();
target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source });
Assert.False(source.SetterCalled);
}
[Fact]
public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_Indexer_With_Value()
{
var target = new Class1();
var source = new TestTwoWayBindingViewModel() { [0] = 1 };
source.ResetSetterCalled();
target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source });
Assert.False(source.SetterCalled);
}
/// <summary>
/// Returns an observable that returns a single value but does not complete.
/// </summary>
@ -943,6 +967,11 @@ namespace Avalonia.Base.UnitTests
}
public bool SetterCalled { get; private set; }
public void ResetSetterCalled()
{
SetterCalled = false;
}
}
}
}

29
tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs

@ -0,0 +1,29 @@
using System;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Base.UnitTests.Utilities;
public class UriExtensionsTests
{
[Fact]
public void Assembly_Name_From_Query_Parsed()
{
const string key = "assembly";
const string value = "Avalonia.Themes.Default";
var uri = new Uri($"resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?{key}={value}");
var name = uri.GetAssemblyNameFromQuery();
Assert.Equal(value, name);
}
[Fact]
public void Assembly_Name_From_Empty_Query_Not_Parsed()
{
var uri = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml");
var name = uri.GetAssemblyNameFromQuery();
Assert.Equal(string.Empty, name);
}
}

23
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs

@ -353,6 +353,29 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Null(target.Host);
}
[Fact]
public void Content_Should_Become_DataContext_When_ControlTemplate_Is_Not_Null()
{
var (target, _) = CreateTarget();
var textBlock = new TextBlock
{
[!TextBlock.TextProperty] = new Binding("Name"),
};
var canvas = new Canvas()
{
Name = "Canvas"
};
target.ContentTemplate = new FuncDataTemplate<Canvas>((_, __) => textBlock);
target.Content = canvas;
Assert.NotNull(target.DataContext);
Assert.Equal(canvas, target.DataContext);
Assert.Equal("Canvas", textBlock.Text);
}
(ContentPresenter presenter, ContentControl templatedParent) CreateTarget()
{
var templatedParent = new ContentControl

31
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@ -1064,6 +1064,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
}
}
[Fact]
public void Binds_To_Self_In_Style()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Button'>
<Setter Property='IsVisible' Value='{CompiledBinding $self.IsEnabled}' />
</Style>
</Window.Styles>
<Button Name='button' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Assert.True(button.IsVisible);
button.IsEnabled = false;
Assert.False(button.IsVisible);
}
}
[Fact]
public void SupportsMethodBindingAsDelegate()
{

31
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

@ -183,6 +183,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void Binding_To_Self_In_Style_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Button'>
<Setter Property='IsVisible' Value='{Binding $self.IsEnabled}' />
</Style>
</Window.Styles>
<Button Name='button' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Assert.True(button.IsVisible);
button.IsEnabled = false;
Assert.False(button.IsVisible);
}
}
[Fact]
public void Stream_Binding_To_Observable_Works()
{

62
tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs

@ -0,0 +1,62 @@
using System;
using System.Reflection;
using Avalonia.PlatformSupport.Internal;
using Moq;
using Xunit;
namespace Avalonia.PlatformSupport.UnitTests;
public class AssetLoaderTests
{
public class MockAssembly : Assembly {}
private const string AssemblyNameWithWhitespace = "Awesome Library";
private const string AssemblyNameWithNonAscii = "Какое-то-название";
static AssetLoaderTests()
{
var resolver = Mock.Of<AssemblyDescriptorResolver>();
var descriptor = CreateAssemblyDescriptor(AssemblyNameWithWhitespace);
Mock.Get(resolver).Setup(x => x.GetAssembly(AssemblyNameWithWhitespace)).Returns(descriptor);
descriptor = CreateAssemblyDescriptor(AssemblyNameWithNonAscii);
Mock.Get(resolver).Setup(x => x.GetAssembly(AssemblyNameWithNonAscii)).Returns(descriptor);
AssetLoader.SetAssemblyDescriptorResolver(resolver);
}
[Fact]
public void AssemblyName_With_Whitespace_Should_Load_Resm()
{
var uri = new Uri($"resm:Avalonia.Base.UnitTests.Assets.something?assembly={AssemblyNameWithWhitespace}");
var loader = new AssetLoader();
var assemblyActual = loader.GetAssembly(uri, null);
Assert.Equal(AssemblyNameWithWhitespace, assemblyActual?.FullName);
}
[Fact]
public void AssemblyName_With_Non_ASCII_Should_Load_Avares()
{
var uri = new Uri($"avares://{AssemblyNameWithNonAscii}/Assets/something");
var loader = new AssetLoader();
var assemblyActual = loader.GetAssembly(uri, null);
Assert.Equal(AssemblyNameWithNonAscii, assemblyActual?.FullName);
}
private static AssemblyDescriptor CreateAssemblyDescriptor(string assemblyName)
{
var assembly = Mock.Of<MockAssembly>();
Mock.Get(assembly).Setup(x => x.GetName()).Returns(new AssemblyName(assemblyName));
Mock.Get(assembly).Setup(x => x.FullName).Returns(assemblyName);
var descriptor = Mock.Of<AssemblyDescriptor>();
Mock.Get(descriptor).Setup(x => x.Assembly).Returns(assembly);
return descriptor;
}
}

24
tests/Avalonia.PlatformSupport.UnitTests/Avalonia.PlatformSupport.UnitTests.csproj

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\UnitTests.NetFX.props" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
</Project>

5
tests/Avalonia.UnitTests/MockAssetLoader.cs

@ -5,6 +5,7 @@ using System.Linq;
using System.Reflection;
using System.Text;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.UnitTests
{
@ -39,7 +40,9 @@ namespace Avalonia.UnitTests
public IEnumerable<Uri> GetAssets(Uri uri, Uri baseUri)
{
return _assets.Keys.Where(x => x.AbsolutePath.Contains(uri.AbsolutePath));
var absPath = uri.GetUnescapeAbsolutePath();
return _assets.Keys.Where(
x => x.GetUnescapeAbsolutePath().IndexOf(absPath, StringComparison.Ordinal) >= 0);
}
public void SetDefaultAssembly(Assembly asm)

45
tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs

@ -12,24 +12,28 @@ namespace Avalonia.Visuals.UnitTests.Media.Fonts
public class FontFamilyLoaderTests : IDisposable
{
private const string AssetMyFontRegular = AssetLocation + ".MyFont Regular.ttf" + Assembly + FontName;
private const string FontName = "#MyFont";
private const string Assembly = "?assembly=Avalonia.Visuals.UnitTests";
private const string AssetLocation = "resm:Avalonia.Visuals.UnitTests.Assets";
private const string AssetLocationAvares = "avares://Avalonia.Visuals.UnitTests";
private const string AssetYourFileName = "/Assets/YourFont.ttf";
private const string AssetYourFontAvares = AssetLocationAvares + AssetYourFileName;
private readonly IDisposable _testApplication;
public FontFamilyLoaderTests()
{
const string AssetMyFontRegular = AssetLocation + ".MyFont-Regular.ttf" + Assembly + FontName;
const string AssetMyFontBold = AssetLocation + ".MyFont-Bold.ttf" + Assembly + FontName;
const string AssetMyFontBold = AssetLocation + ".MyFont Bold.ttf" + Assembly + FontName;
const string AssetYourFont = AssetLocation + ".YourFont.ttf" + Assembly + FontName;
var fontAssets = new[]
{
(AssetMyFontRegular, "AssetData"),
(AssetMyFontBold, "AssetData"),
(AssetYourFont, "AssetData")
};
{
(AssetMyFontRegular, "AssetData"),
(AssetMyFontBold, "AssetData"),
(AssetYourFont, "AssetData"),
(AssetYourFontAvares, "AssetData")
};
_testApplication = StartWithResources(fontAssets);
}
@ -42,10 +46,18 @@ namespace Avalonia.Visuals.UnitTests.Media.Fonts
[Fact]
public void Should_Load_Single_FontAsset()
{
const string FontAsset = AssetLocation + ".MyFont-Regular.ttf" + Assembly + FontName;
var source = new Uri(AssetMyFontRegular, UriKind.RelativeOrAbsolute);
var key = new FontFamilyKey(source);
var source = new Uri(FontAsset, UriKind.RelativeOrAbsolute);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
Assert.Single(fontAssets);
}
[Fact]
public void Should_Load_Single_FontAsset_Avares_Without_BaseUri()
{
var source = new Uri(AssetYourFontAvares);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
@ -54,10 +66,21 @@ namespace Avalonia.Visuals.UnitTests.Media.Fonts
}
[Fact]
public void Should_Load_Matching_Assets()
public void Should_Load_Single_FontAsset_Avares_With_BaseUri()
{
var source = new Uri(AssetLocation + ".MyFont-*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute);
var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute);
var baseUri = new Uri(AssetLocationAvares);
var key = new FontFamilyKey(source, baseUri);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
Assert.Single(fontAssets);
}
[Fact]
public void Should_Load_Matching_Assets()
{
var source = new Uri(AssetLocation + ".MyFont*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key).ToArray();

Loading…
Cancel
Save