committed by
GitHub
43 changed files with 1332 additions and 350 deletions
@ -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(); |
|||
} |
|||
} |
|||
@ -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="<" /> |
|||
<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> |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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."); |
|||
} |
|||
} |
|||
} |
|||
@ -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 ""; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Avalonia.PlatformSupport.Internal; |
|||
|
|||
internal static class Constants |
|||
{ |
|||
public static string AvaloniaResourceName => "!AvaloniaResources"; |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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> |
|||
Loading…
Reference in new issue