committed by
GitHub
126 changed files with 3265 additions and 701 deletions
@ -1,5 +1,5 @@ |
|||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="Moq" Version="4.7.99" /> |
<PackageReference Include="Moq" Version="4.14.1" /> |
||||
</ItemGroup> |
</ItemGroup> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -1,7 +0,0 @@ |
|||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|
||||
<ItemGroup> |
|
||||
<PackageReference Include="Serilog" Version="2.5.0" /> |
|
||||
<PackageReference Include="Serilog.Sinks.Trace" Version="2.1.0" /> |
|
||||
<PackageReference Include="Serilog.Sinks.Debug" Version="1.0.0" /> |
|
||||
</ItemGroup> |
|
||||
</Project> |
|
||||
@ -0,0 +1,79 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="ControlCatalog.Pages.ToggleSwitchPage" Margin="5"> |
||||
|
<StackPanel Width="500" HorizontalAlignment="Center"> |
||||
|
<TextBlock Text="Simple ToggleSwitch" Classes="header"/> |
||||
|
<Border Classes="Thin"> |
||||
|
<StackPanel> |
||||
|
<ToggleSwitch Margin="10"/> |
||||
|
<TextBox Text="<ToggleSwitch/>" Classes="CodeBox"/> |
||||
|
</StackPanel> |
||||
|
</Border> |
||||
|
|
||||
|
<TextBlock Text="headered ToggleSwitch" Classes="header"/> |
||||
|
|
||||
|
<Border Classes="Thin"> |
||||
|
<StackPanel> |
||||
|
<ToggleSwitch Content="headered" IsChecked="true" Margin="10"/> |
||||
|
<TextBox Classes="CodeBox" |
||||
|
Text="<ToggleSwitch>headered</ToggleSwitch>"/> |
||||
|
</StackPanel> |
||||
|
</Border> |
||||
|
|
||||
|
<TextBlock Text="Custom content ToggleSwitch" Classes="header"/> |
||||
|
|
||||
|
<Border Classes="Thin"> |
||||
|
<StackPanel> |
||||
|
<ToggleSwitch Content="Custom" |
||||
|
OnContent="On" |
||||
|
OffContent="Off" |
||||
|
Margin="10"/> |
||||
|
|
||||
|
<TextBox Text="<ToggleSwitch Content="Custom" |
||||
|
ContentOn="On" |
||||
|
ContentOff="Off" />" |
||||
|
Classes="CodeBox"/> |
||||
|
</StackPanel> |
||||
|
</Border> |
||||
|
|
||||
|
<TextBlock Text="Image content ToggleSwitch" Classes="header"/> |
||||
|
|
||||
|
<Border Classes="Thin"> |
||||
|
<StackPanel> |
||||
|
<ToggleSwitch Content="Just Click!" Margin="10"> |
||||
|
<ToggleSwitch.OnContent> |
||||
|
<Image Source="/Assets/hirsch-899118_640.jpg" Height="32"/> |
||||
|
</ToggleSwitch.OnContent> |
||||
|
|
||||
|
<ToggleSwitch.OffContent> |
||||
|
<Image Source="/Assets/delicate-arch-896885_640.jpg" Height="32"/> |
||||
|
</ToggleSwitch.OffContent> |
||||
|
|
||||
|
</ToggleSwitch> |
||||
|
</StackPanel> |
||||
|
</Border> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<UserControl.Styles > |
||||
|
<Style Selector="TextBox.CodeBox" > |
||||
|
<Setter Property="Padding" Value="10"/> |
||||
|
<Setter Property="IsReadOnly" Value="True"/> |
||||
|
<Setter Property="BorderBrush" Value="Transparent"/> |
||||
|
<Setter Property="FontSize" Value="14"/> |
||||
|
<Setter Property="IsEnabled" Value="true"/> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="TextBlock.header"> |
||||
|
<Setter Property="FontSize" Value="18"/> |
||||
|
<Setter Property="Margin" Value="0 20 0 20"/> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Border.Thin"> |
||||
|
<Setter Property="BorderBrush" Value="Gray"/> |
||||
|
<Setter Property="BorderThickness" Value="0.5"/> |
||||
|
<Setter Property="CornerRadius" Value="2"/> |
||||
|
</Style> |
||||
|
|
||||
|
</UserControl.Styles> |
||||
|
</UserControl> |
||||
|
|
||||
@ -0,0 +1,19 @@ |
|||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
|
||||
|
namespace ControlCatalog.Pages |
||||
|
{ |
||||
|
public class ToggleSwitchPage : UserControl |
||||
|
{ |
||||
|
public ToggleSwitchPage() |
||||
|
{ |
||||
|
this.InitializeComponent(); |
||||
|
} |
||||
|
|
||||
|
private void InitializeComponent() |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,115 @@ |
|||||
|
<UserControl |
||||
|
xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="RenderDemo.Pages.TransitionsPage"> |
||||
|
<UserControl.Styles> |
||||
|
<Styles> |
||||
|
<Styles.Resources> |
||||
|
<Template x:Key="Acorn"> |
||||
|
<Path Fill="White" Stretch="Uniform" |
||||
|
Data="F1 M 16.6309,18.6563C 17.1309, |
||||
|
8.15625 29.8809,14.1563 29.8809, |
||||
|
14.1563C 30.8809,11.1563 34.1308, |
||||
|
11.4063 34.1308,11.4063C 33.5,12 |
||||
|
34.6309,13.1563 34.6309,13.1563C |
||||
|
32.1309,13.1562 31.1309,14.9062 |
||||
|
31.1309,14.9062C 41.1309,23.9062 |
||||
|
32.6309,27.9063 32.6309,27.9062C |
||||
|
24.6309,24.9063 21.1309,22.1562 |
||||
|
16.6309,18.6563 Z M 16.6309,19.9063C |
||||
|
21.6309,24.1563 25.1309,26.1562 |
||||
|
31.6309,28.6562C 31.6309,28.6562 |
||||
|
26.3809,39.1562 18.3809,36.1563C |
||||
|
18.3809,36.1563 18,38 16.3809,36.9063C |
||||
|
15,36 16.3809,34.9063 16.3809,34.9063C |
||||
|
16.3809,34.9063 10.1309,30.9062 16.6309,19.9063 Z"/> |
||||
|
</Template> |
||||
|
<Template x:Key="Heart"> |
||||
|
<Path Fill="Red" Stretch="Uniform" Data=" |
||||
|
M 272.70141,238.71731 |
||||
|
C 206.46141,238.71731 152.70146,292.4773 152.70146,358.71731 |
||||
|
C 152.70146,493.47282 288.63461,528.80461 381.26391,662.02535 |
||||
|
C 468.83815,529.62199 609.82641,489.17075 609.82641,358.71731 |
||||
|
C 609.82641,292.47731 556.06651,238.7173 489.82641,238.71731 |
||||
|
C 441.77851,238.71731 400.42481,267.08774 381.26391,307.90481 |
||||
|
C 362.10311,267.08773 320.74941,238.7173 272.70141,238.71731 z "/> |
||||
|
</Template> |
||||
|
</Styles.Resources> |
||||
|
|
||||
|
<Style Selector="Border.Test"> |
||||
|
<Setter Property="Margin" Value="15"/> |
||||
|
<Setter Property="Width" Value="100"/> |
||||
|
<Setter Property="Height" Value="100"/> |
||||
|
<Setter Property="Child" Value="{StaticResource Acorn}"/> |
||||
|
<Setter Property="Transitions"> |
||||
|
<Transitions> |
||||
|
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:1" /> |
||||
|
</Transitions> |
||||
|
</Setter> |
||||
|
<Setter Property="RenderTransform" Value="none" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Border.Rect1:pointerover"> |
||||
|
<Setter Property="RenderTransform" Value="rotate(120deg) scale(1.5)" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Border.Rect2:pointerover"> |
||||
|
<Setter Property="RenderTransform" Value="scale(0.8)" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Border.Rect3"> |
||||
|
<Setter Property="Child" Value="{StaticResource Heart}"/> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Border.Rect3:pointerover"> |
||||
|
<Setter Property="RenderTransform" Value="rotate(1turn)" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Border.Rect4:pointerover"> |
||||
|
<Setter Property="RenderTransform" Value="translateY(-100px)" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Border.Rect5:pointerover"> |
||||
|
<Setter Property="RenderTransform" Value="skewX(-20deg)" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Border.Rect5:pointerover"> |
||||
|
<Setter Property="RenderTransform" Value="skewX(-20deg)" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Border.Rect6"> |
||||
|
<Setter Property="Transitions"> |
||||
|
<Transitions> |
||||
|
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.5" Delay="0:0:1"/> |
||||
|
</Transitions> |
||||
|
</Setter> |
||||
|
<Setter Property="RenderTransform" Value="scale(0.8)" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Border.Rect6:pointerover"> |
||||
|
<Setter Property="RenderTransform" Value="none" /> |
||||
|
</Style> |
||||
|
|
||||
|
</Styles> |
||||
|
</UserControl.Styles> |
||||
|
|
||||
|
<Grid> |
||||
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" ClipToBounds="False"> |
||||
|
<StackPanel.Clock> |
||||
|
<Clock /> |
||||
|
</StackPanel.Clock> |
||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"> |
||||
|
<TextBlock VerticalAlignment="Center">Hover to activate Transform Keyframe Animations.</TextBlock> |
||||
|
<Button Content="{Binding PlayStateText}" Command="{Binding TogglePlayState}" Click="ToggleClock" /> |
||||
|
</StackPanel> |
||||
|
<WrapPanel ClipToBounds="False"> |
||||
|
<Border Classes="Test Rect1" Background="DarkRed"/> |
||||
|
<Border Classes="Test Rect2" Background="Magenta"/> |
||||
|
<Border Classes="Test Rect3"/> |
||||
|
<Border Classes="Test Rect4" Background="Navy"/> |
||||
|
<Border Classes="Test Rect5" Background="SeaGreen"/> |
||||
|
<Border Classes="Test Rect6" Background="Orange"/> |
||||
|
</WrapPanel> |
||||
|
</StackPanel> |
||||
|
</Grid> |
||||
|
</UserControl> |
||||
@ -0,0 +1,37 @@ |
|||||
|
using Avalonia.Animation; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
using RenderDemo.ViewModels; |
||||
|
|
||||
|
namespace RenderDemo.Pages |
||||
|
{ |
||||
|
public class TransitionsPage : UserControl |
||||
|
{ |
||||
|
public TransitionsPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
this.DataContext = new AnimationsPageViewModel(); |
||||
|
} |
||||
|
|
||||
|
private void InitializeComponent() |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
private void ToggleClock(object sender, RoutedEventArgs args) |
||||
|
{ |
||||
|
var button = sender as Button; |
||||
|
var clock = button.Clock; |
||||
|
|
||||
|
if (clock.PlayState == PlayState.Run) |
||||
|
{ |
||||
|
clock.PlayState = PlayState.Pause; |
||||
|
} |
||||
|
else if (clock.PlayState == PlayState.Pause) |
||||
|
{ |
||||
|
clock.PlayState = PlayState.Run; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,179 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
using System.Text; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Logging |
||||
|
{ |
||||
|
public class DebugLogSink : ILogSink |
||||
|
{ |
||||
|
private readonly LogEventLevel _level; |
||||
|
private readonly IList<string> _areas; |
||||
|
|
||||
|
public DebugLogSink( |
||||
|
LogEventLevel minimumLevel, |
||||
|
IList<string> areas = null) |
||||
|
{ |
||||
|
_level = minimumLevel; |
||||
|
_areas = areas?.Count > 0 ? areas : null; |
||||
|
} |
||||
|
|
||||
|
public bool IsEnabled(LogEventLevel level, string area) |
||||
|
{ |
||||
|
return level >= _level && (_areas?.Contains(area) ?? true); |
||||
|
} |
||||
|
|
||||
|
public void Log(LogEventLevel level, string area, object source, string messageTemplate) |
||||
|
{ |
||||
|
if (IsEnabled(level, area)) |
||||
|
{ |
||||
|
Debug.WriteLine(Format<object, object, object>(area, messageTemplate, source)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Log<T0>(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0) |
||||
|
{ |
||||
|
if (IsEnabled(level, area)) |
||||
|
{ |
||||
|
Debug.WriteLine(Format<T0, object, object>(area, messageTemplate, source, propertyValue0)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Log<T0, T1>(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1) |
||||
|
{ |
||||
|
if (IsEnabled(level, area)) |
||||
|
{ |
||||
|
Debug.WriteLine(Format<T0, T1, object>(area, messageTemplate, source, propertyValue0, propertyValue1)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Log<T0, T1, T2>(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) |
||||
|
{ |
||||
|
if (IsEnabled(level, area)) |
||||
|
{ |
||||
|
Debug.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1, propertyValue2)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues) |
||||
|
{ |
||||
|
if (IsEnabled(level, area)) |
||||
|
{ |
||||
|
Debug.WriteLine(Format(area, messageTemplate, source, propertyValues)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static string Format<T0, T1, T2>( |
||||
|
string area, |
||||
|
string template, |
||||
|
object source, |
||||
|
T0 v0 = default, |
||||
|
T1 v1 = default, |
||||
|
T2 v2 = default) |
||||
|
{ |
||||
|
var result = new StringBuilder(template.Length); |
||||
|
var r = new CharacterReader(template.AsSpan()); |
||||
|
var i = 0; |
||||
|
|
||||
|
result.Append('['); |
||||
|
result.Append(area); |
||||
|
result.Append("] "); |
||||
|
|
||||
|
while (!r.End) |
||||
|
{ |
||||
|
var c = r.Take(); |
||||
|
|
||||
|
if (c != '{') |
||||
|
{ |
||||
|
result.Append(c); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (r.Peek != '{') |
||||
|
{ |
||||
|
result.Append('\''); |
||||
|
result.Append(i++ switch |
||||
|
{ |
||||
|
0 => v0, |
||||
|
1 => v1, |
||||
|
2 => v2, |
||||
|
_ => null |
||||
|
}); |
||||
|
result.Append('\''); |
||||
|
r.TakeUntil('}'); |
||||
|
r.Take(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
result.Append('{'); |
||||
|
r.Take(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (source is object) |
||||
|
{ |
||||
|
result.Append(" ("); |
||||
|
result.Append(source.GetType().Name); |
||||
|
result.Append(" #"); |
||||
|
result.Append(source.GetHashCode()); |
||||
|
result.Append(')'); |
||||
|
} |
||||
|
|
||||
|
return result.ToString(); |
||||
|
} |
||||
|
|
||||
|
private static string Format( |
||||
|
string area, |
||||
|
string template, |
||||
|
object source, |
||||
|
object[] v) |
||||
|
{ |
||||
|
var result = new StringBuilder(template.Length); |
||||
|
var r = new CharacterReader(template.AsSpan()); |
||||
|
var i = 0; |
||||
|
|
||||
|
result.Append('['); |
||||
|
result.Append(area); |
||||
|
result.Append(']'); |
||||
|
|
||||
|
while (!r.End) |
||||
|
{ |
||||
|
var c = r.Take(); |
||||
|
|
||||
|
if (c != '{') |
||||
|
{ |
||||
|
result.Append(c); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (r.Peek != '{') |
||||
|
{ |
||||
|
result.Append('\''); |
||||
|
result.Append(i < v.Length ? v[i++] : null); |
||||
|
result.Append('\''); |
||||
|
r.TakeUntil('}'); |
||||
|
r.Take(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
result.Append('{'); |
||||
|
r.Take(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (source is object) |
||||
|
{ |
||||
|
result.Append('('); |
||||
|
result.Append(source.GetType().Name); |
||||
|
result.Append(" #"); |
||||
|
result.Append(source.GetHashCode()); |
||||
|
result.Append(')'); |
||||
|
} |
||||
|
|
||||
|
return result.ToString(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Globalization; |
||||
|
using Avalonia.Controls.Primitives; |
||||
|
using Avalonia.Data.Converters; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Controls.Converters |
||||
|
{ |
||||
|
public class MenuScrollingVisibilityConverter : IMultiValueConverter |
||||
|
{ |
||||
|
public static readonly MenuScrollingVisibilityConverter Instance = new MenuScrollingVisibilityConverter(); |
||||
|
|
||||
|
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture) |
||||
|
{ |
||||
|
if (parameter == null || |
||||
|
values == null || |
||||
|
values.Count != 4 || |
||||
|
!(values[0] is ScrollBarVisibility visiblity) || |
||||
|
!(values[1] is double offset) || |
||||
|
!(values[2] is double extent) || |
||||
|
!(values[3] is double viewport)) |
||||
|
{ |
||||
|
return AvaloniaProperty.UnsetValue; |
||||
|
} |
||||
|
|
||||
|
if (visiblity == ScrollBarVisibility.Auto) |
||||
|
{ |
||||
|
if (extent == viewport) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
double target; |
||||
|
|
||||
|
if (parameter is double d) |
||||
|
{ |
||||
|
target = d; |
||||
|
} |
||||
|
else if (parameter is string s) |
||||
|
{ |
||||
|
target = double.Parse(s, NumberFormatInfo.InvariantInfo); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return AvaloniaProperty.UnsetValue; |
||||
|
} |
||||
|
|
||||
|
// Calculate the percent so that we can see if we are near the edge of the range
|
||||
|
double percent = MathUtilities.Clamp(offset * 100.0 / (extent - viewport), 0, 100); |
||||
|
|
||||
|
if (MathUtilities.AreClose(percent, target)) |
||||
|
{ |
||||
|
// We are at the end of the range, so no need for this button to be shown
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Logging; |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
public static class LoggingExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Logs Avalonia events to the <see cref="System.Diagnostics.Debug"/> sink.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The application class type.</typeparam>
|
||||
|
/// <param name="builder">The app builder instance.</param>
|
||||
|
/// <param name="level">The minimum level to log.</param>
|
||||
|
/// <param name="areas">The areas to log. Valid values are listed in <see cref="LogArea"/>.</param>
|
||||
|
/// <returns>The app builder instance.</returns>
|
||||
|
public static T LogToDebug<T>( |
||||
|
this T builder, |
||||
|
LogEventLevel level = LogEventLevel.Warning, |
||||
|
params string[] areas) |
||||
|
where T : AppBuilderBase<T>, new() |
||||
|
{ |
||||
|
Logger.Sink = new DebugLogSink(level, areas); |
||||
|
return builder; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,10 +0,0 @@ |
|||||
<Project Sdk="Microsoft.NET.Sdk"> |
|
||||
<PropertyGroup> |
|
||||
<TargetFramework>netstandard2.0</TargetFramework> |
|
||||
</PropertyGroup> |
|
||||
<ItemGroup> |
|
||||
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" /> |
|
||||
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" /> |
|
||||
</ItemGroup> |
|
||||
<Import Project="..\..\build\Serilog.props" /> |
|
||||
</Project> |
|
||||
@ -1,103 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Controls; |
|
||||
using Serilog; |
|
||||
using Serilog.Configuration; |
|
||||
using Serilog.Filters; |
|
||||
using SerilogLevel = Serilog.Events.LogEventLevel; |
|
||||
|
|
||||
namespace Avalonia.Logging.Serilog |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Extension methods for Serilog logging.
|
|
||||
/// </summary>
|
|
||||
public static class SerilogExtensions |
|
||||
{ |
|
||||
private const string DefaultTemplate = "[{Area}] {Message} ({SourceType} #{SourceHash})"; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Logs Avalonia events to the <see cref="System.Diagnostics.Debug"/> sink.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="T">The application class type.</typeparam>
|
|
||||
/// <param name="builder">The app builder instance.</param>
|
|
||||
/// <param name="level">The minimum level to log.</param>
|
|
||||
/// <returns>The app builder instance.</returns>
|
|
||||
public static T LogToDebug<T>( |
|
||||
this T builder, |
|
||||
LogEventLevel level = LogEventLevel.Warning) |
|
||||
where T : AppBuilderBase<T>, new() |
|
||||
{ |
|
||||
SerilogLogger.Initialize(new LoggerConfiguration() |
|
||||
.MinimumLevel.Is((SerilogLevel)level) |
|
||||
.Enrich.FromLogContext() |
|
||||
.WriteTo.Debug(outputTemplate: DefaultTemplate) |
|
||||
.CreateLogger()); |
|
||||
return builder; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Logs Avalonia events to the <see cref="System.Diagnostics.Debug"/> sink.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="T">The application class type.</typeparam>
|
|
||||
/// <param name="builder">The app builder instance.</param>
|
|
||||
/// <param name="area">The area to log. Valid values are listed in <see cref="LogArea"/>.</param>
|
|
||||
/// <param name="level">The minimum level to log.</param>
|
|
||||
/// <returns>The app builder instance.</returns>
|
|
||||
public static T LogToDebug<T>( |
|
||||
this T builder, |
|
||||
string area, |
|
||||
LogEventLevel level = LogEventLevel.Warning) |
|
||||
where T : AppBuilderBase<T>, new() |
|
||||
{ |
|
||||
SerilogLogger.Initialize(new LoggerConfiguration() |
|
||||
.MinimumLevel.Is((SerilogLevel)level) |
|
||||
.Filter.ByIncludingOnly(Matching.WithProperty("Area", area)) |
|
||||
.Enrich.FromLogContext() |
|
||||
.WriteTo.Debug(outputTemplate: DefaultTemplate) |
|
||||
.CreateLogger()); |
|
||||
return builder; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Logs Avalonia events to the <see cref="System.Diagnostics.Trace"/> sink.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="T">The application class type.</typeparam>
|
|
||||
/// <param name="builder">The app builder instance.</param>
|
|
||||
/// <param name="level">The minimum level to log.</param>
|
|
||||
/// <returns>The app builder instance.</returns>
|
|
||||
public static T LogToTrace<T>( |
|
||||
this T builder, |
|
||||
LogEventLevel level = LogEventLevel.Warning) |
|
||||
where T : AppBuilderBase<T>, new() |
|
||||
{ |
|
||||
SerilogLogger.Initialize(new LoggerConfiguration() |
|
||||
.MinimumLevel.Is((SerilogLevel)level) |
|
||||
.Enrich.FromLogContext() |
|
||||
.WriteTo.Trace(outputTemplate: DefaultTemplate) |
|
||||
.CreateLogger()); |
|
||||
return builder; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Logs Avalonia events to the <see cref="System.Diagnostics.Trace"/> sink.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="T">The application class type.</typeparam>
|
|
||||
/// <param name="builder">The app builder instance.</param>
|
|
||||
/// <param name="area">The area to log. Valid values are listed in <see cref="LogArea"/>.</param>
|
|
||||
/// <param name="level">The minimum level to log.</param>
|
|
||||
/// <returns>The app builder instance.</returns>
|
|
||||
public static T LogToTrace<T>( |
|
||||
this T builder, |
|
||||
string area, |
|
||||
LogEventLevel level = LogEventLevel.Warning) |
|
||||
where T : AppBuilderBase<T>, new() |
|
||||
{ |
|
||||
SerilogLogger.Initialize(new LoggerConfiguration() |
|
||||
.MinimumLevel.Is((SerilogLevel)level) |
|
||||
.Filter.ByIncludingOnly(Matching.WithProperty("Area", area)) |
|
||||
.Enrich.FromLogContext() |
|
||||
.WriteTo.Trace(outputTemplate: DefaultTemplate) |
|
||||
.CreateLogger()); |
|
||||
return builder; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,151 +0,0 @@ |
|||||
using System; |
|
||||
using Serilog; |
|
||||
using Serilog.Context; |
|
||||
using AvaloniaLogEventLevel = Avalonia.Logging.LogEventLevel; |
|
||||
using SerilogLogEventLevel = Serilog.Events.LogEventLevel; |
|
||||
|
|
||||
namespace Avalonia.Logging.Serilog |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Sends log output to serilog.
|
|
||||
/// </summary>
|
|
||||
public class SerilogLogger : ILogSink |
|
||||
{ |
|
||||
private readonly ILogger _output; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="SerilogLogger"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="output">The serilog logger to use.</param>
|
|
||||
public SerilogLogger(ILogger output) |
|
||||
{ |
|
||||
_output = output; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes the Avalonia logging with a new instance of a <see cref="SerilogLogger"/>.
|
|
||||
/// </summary>
|
|
||||
/// <param name="output">The serilog logger to use.</param>
|
|
||||
public static void Initialize(ILogger output) |
|
||||
{ |
|
||||
Logger.Sink = new SerilogLogger(output); |
|
||||
} |
|
||||
|
|
||||
public bool IsEnabled(LogEventLevel level) |
|
||||
{ |
|
||||
return _output.IsEnabled((SerilogLogEventLevel)level); |
|
||||
} |
|
||||
|
|
||||
public void Log( |
|
||||
LogEventLevel level, |
|
||||
string area, |
|
||||
object source, |
|
||||
string messageTemplate) |
|
||||
{ |
|
||||
Contract.Requires<ArgumentNullException>(area != null); |
|
||||
Contract.Requires<ArgumentNullException>(messageTemplate != null); |
|
||||
|
|
||||
using (PushLogContextProperties(area, source)) |
|
||||
{ |
|
||||
_output.Write((SerilogLogEventLevel)level, messageTemplate); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Log<T0>( |
|
||||
LogEventLevel level, |
|
||||
string area, object source, |
|
||||
string messageTemplate, |
|
||||
T0 propertyValue0) |
|
||||
{ |
|
||||
Contract.Requires<ArgumentNullException>(area != null); |
|
||||
Contract.Requires<ArgumentNullException>(messageTemplate != null); |
|
||||
|
|
||||
using (PushLogContextProperties(area, source)) |
|
||||
{ |
|
||||
_output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Log<T0, T1>( |
|
||||
LogEventLevel level, |
|
||||
string area, |
|
||||
object source, |
|
||||
string messageTemplate, |
|
||||
T0 propertyValue0, |
|
||||
T1 propertyValue1) |
|
||||
{ |
|
||||
Contract.Requires<ArgumentNullException>(area != null); |
|
||||
Contract.Requires<ArgumentNullException>(messageTemplate != null); |
|
||||
|
|
||||
using (PushLogContextProperties(area, source)) |
|
||||
{ |
|
||||
_output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0, propertyValue1); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Log<T0, T1, T2>( |
|
||||
LogEventLevel level, |
|
||||
string area, |
|
||||
object source, |
|
||||
string messageTemplate, |
|
||||
T0 propertyValue0, |
|
||||
T1 propertyValue1, |
|
||||
T2 propertyValue2) |
|
||||
{ |
|
||||
Contract.Requires<ArgumentNullException>(area != null); |
|
||||
Contract.Requires<ArgumentNullException>(messageTemplate != null); |
|
||||
|
|
||||
using (PushLogContextProperties(area, source)) |
|
||||
{ |
|
||||
_output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0, propertyValue1, propertyValue2); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void Log( |
|
||||
AvaloniaLogEventLevel level, |
|
||||
string area, |
|
||||
object source, |
|
||||
string messageTemplate, |
|
||||
params object[] propertyValues) |
|
||||
{ |
|
||||
Contract.Requires<ArgumentNullException>(area != null); |
|
||||
Contract.Requires<ArgumentNullException>(messageTemplate != null); |
|
||||
|
|
||||
using (PushLogContextProperties(area, source)) |
|
||||
{ |
|
||||
_output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValues); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static LogContextDisposable PushLogContextProperties(string area, object source) |
|
||||
{ |
|
||||
return new LogContextDisposable( |
|
||||
LogContext.PushProperty("Area", area), |
|
||||
LogContext.PushProperty("SourceType", source?.GetType()), |
|
||||
LogContext.PushProperty("SourceHash", source?.GetHashCode()) |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
private readonly struct LogContextDisposable : IDisposable |
|
||||
{ |
|
||||
private readonly IDisposable _areaDisposable; |
|
||||
private readonly IDisposable _sourceTypeDisposable; |
|
||||
private readonly IDisposable _sourceHashDisposable; |
|
||||
|
|
||||
public LogContextDisposable(IDisposable areaDisposable, IDisposable sourceTypeDisposable, IDisposable sourceHashDisposable) |
|
||||
{ |
|
||||
_areaDisposable = areaDisposable; |
|
||||
_sourceTypeDisposable = sourceTypeDisposable; |
|
||||
_sourceHashDisposable = sourceHashDisposable; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
_areaDisposable.Dispose(); |
|
||||
_sourceTypeDisposable.Dispose(); |
|
||||
_sourceHashDisposable.Dispose(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,47 +1,105 @@ |
|||||
<Style xmlns="https://github.com/avaloniaui" Selector="ScrollViewer"> |
<Styles xmlns="https://github.com/avaloniaui" |
||||
<Setter Property="Background" |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
Value="Transparent" /> |
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"> |
||||
<Setter Property="Template"> |
<Style Selector="ScrollViewer"> |
||||
<ControlTemplate> |
<Setter Property="Background" |
||||
<Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto"> |
Value="Transparent" /> |
||||
<ScrollContentPresenter Name="PART_ContentPresenter" |
<Setter Property="Template"> |
||||
Background="{TemplateBinding Background}" |
<ControlTemplate> |
||||
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}" |
<Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto"> |
||||
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" |
<ScrollContentPresenter Name="PART_ContentPresenter" |
||||
Content="{TemplateBinding Content}" |
Background="{TemplateBinding Background}" |
||||
Extent="{TemplateBinding Extent, Mode=TwoWay}" |
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}" |
||||
Margin="{TemplateBinding Padding}" |
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" |
||||
Offset="{TemplateBinding Offset, Mode=TwoWay}" |
Content="{TemplateBinding Content}" |
||||
Viewport="{TemplateBinding Viewport, Mode=TwoWay}"> |
Extent="{TemplateBinding Extent, Mode=TwoWay}" |
||||
<ScrollContentPresenter.GestureRecognizers> |
Margin="{TemplateBinding Padding}" |
||||
<ScrollGestureRecognizer |
Offset="{TemplateBinding Offset, Mode=TwoWay}" |
||||
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}" |
Viewport="{TemplateBinding Viewport, Mode=TwoWay}"> |
||||
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" |
<ScrollContentPresenter.GestureRecognizers> |
||||
|
<ScrollGestureRecognizer |
||||
|
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}" |
||||
|
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" |
||||
/> |
/> |
||||
</ScrollContentPresenter.GestureRecognizers> |
</ScrollContentPresenter.GestureRecognizers> |
||||
</ScrollContentPresenter> |
</ScrollContentPresenter> |
||||
<ScrollBar Name="horizontalScrollBar" |
<ScrollBar Name="horizontalScrollBar" |
||||
Orientation="Horizontal" |
Orientation="Horizontal" |
||||
LargeChange="{Binding LargeChange.Width, RelativeSource={RelativeSource TemplatedParent}}" |
LargeChange="{Binding LargeChange.Width, RelativeSource={RelativeSource TemplatedParent}}" |
||||
SmallChange="{Binding SmallChange.Width, RelativeSource={RelativeSource TemplatedParent}}" |
SmallChange="{Binding SmallChange.Width, RelativeSource={RelativeSource TemplatedParent}}" |
||||
Maximum="{TemplateBinding HorizontalScrollBarMaximum}" |
Maximum="{TemplateBinding HorizontalScrollBarMaximum}" |
||||
Value="{TemplateBinding HorizontalScrollBarValue, Mode=TwoWay}" |
Value="{TemplateBinding HorizontalScrollBarValue, Mode=TwoWay}" |
||||
ViewportSize="{TemplateBinding HorizontalScrollBarViewportSize}" |
ViewportSize="{TemplateBinding HorizontalScrollBarViewportSize}" |
||||
Visibility="{TemplateBinding HorizontalScrollBarVisibility}" |
Visibility="{TemplateBinding HorizontalScrollBarVisibility}" |
||||
Grid.Row="1" |
Grid.Row="1" |
||||
Focusable="False"/> |
Focusable="False"/> |
||||
<ScrollBar Name="verticalScrollBar" |
<ScrollBar Name="verticalScrollBar" |
||||
Orientation="Vertical" |
Orientation="Vertical" |
||||
LargeChange="{Binding LargeChange.Height, RelativeSource={RelativeSource TemplatedParent}}" |
LargeChange="{Binding LargeChange.Height, RelativeSource={RelativeSource TemplatedParent}}" |
||||
SmallChange="{Binding SmallChange.Height, RelativeSource={RelativeSource TemplatedParent}}" |
SmallChange="{Binding SmallChange.Height, RelativeSource={RelativeSource TemplatedParent}}" |
||||
Maximum="{TemplateBinding VerticalScrollBarMaximum}" |
Maximum="{TemplateBinding VerticalScrollBarMaximum}" |
||||
Value="{TemplateBinding VerticalScrollBarValue, Mode=TwoWay}" |
Value="{TemplateBinding VerticalScrollBarValue, Mode=TwoWay}" |
||||
ViewportSize="{TemplateBinding VerticalScrollBarViewportSize}" |
ViewportSize="{TemplateBinding VerticalScrollBarViewportSize}" |
||||
Visibility="{TemplateBinding VerticalScrollBarVisibility}" |
Visibility="{TemplateBinding VerticalScrollBarVisibility}" |
||||
Grid.Column="1" |
Grid.Column="1" |
||||
Focusable="False"/> |
Focusable="False"/> |
||||
<Panel Grid.Row="1" Grid.Column="1" Background="{DynamicResource ThemeControlMidBrush}"/> |
<Panel Grid.Row="1" Grid.Column="1" Background="{DynamicResource ThemeControlMidBrush}"/> |
||||
</Grid> |
</Grid> |
||||
</ControlTemplate> |
</ControlTemplate> |
||||
</Setter> |
</Setter> |
||||
</Style> |
</Style> |
||||
|
|
||||
|
<Style Selector="ScrollViewer.menuscroller"> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<DockPanel> |
||||
|
<RepeatButton DockPanel.Dock="Top" |
||||
|
BorderThickness="0" |
||||
|
Background="Transparent" |
||||
|
Command="{Binding LineUp, RelativeSource={RelativeSource TemplatedParent}}"> |
||||
|
<RepeatButton.IsVisible> |
||||
|
<MultiBinding Converter="{x:Static converters:MenuScrollingVisibilityConverter.Instance}" |
||||
|
ConverterParameter="0"> |
||||
|
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="VerticalScrollBarVisibility"/> |
||||
|
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Offset.Y"/> |
||||
|
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Extent.Height"/> |
||||
|
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Viewport.Height"/> |
||||
|
</MultiBinding> |
||||
|
</RepeatButton.IsVisible> |
||||
|
<Path Data="M 0 4 L 8 4 L 4 0 Z"/> |
||||
|
</RepeatButton> |
||||
|
<RepeatButton DockPanel.Dock="Bottom" |
||||
|
BorderThickness="0" |
||||
|
Background="Transparent" |
||||
|
Command="{Binding LineDown, RelativeSource={RelativeSource TemplatedParent}}"> |
||||
|
<RepeatButton.IsVisible> |
||||
|
<MultiBinding Converter="{x:Static converters:MenuScrollingVisibilityConverter.Instance}" |
||||
|
ConverterParameter="100"> |
||||
|
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="VerticalScrollBarVisibility"/> |
||||
|
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Offset.Y"/> |
||||
|
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Extent.Height"/> |
||||
|
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Viewport.Height"/> |
||||
|
</MultiBinding> |
||||
|
</RepeatButton.IsVisible> |
||||
|
<Path Data="M 0 0 L 4 4 L 8 0 Z"/> |
||||
|
</RepeatButton> |
||||
|
<ScrollContentPresenter Name="PART_ContentPresenter" |
||||
|
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}" |
||||
|
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" |
||||
|
Content="{TemplateBinding Content}" |
||||
|
Extent="{TemplateBinding Extent, Mode=TwoWay}" |
||||
|
Margin="{TemplateBinding Padding}" |
||||
|
Offset="{TemplateBinding Offset, Mode=TwoWay}" |
||||
|
Viewport="{TemplateBinding Viewport, Mode=TwoWay}"/> |
||||
|
</DockPanel> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
</Style> |
||||
|
<Style Selector="ScrollViewer.menuscroller /template/ RepeatButton > Path"> |
||||
|
<Setter Property="Fill" Value="{DynamicResource ThemeForegroundLowBrush}" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="ScrollViewer.menuscroller /template/ RepeatButton:pointerover > Path"> |
||||
|
<Setter Property="Fill" Value="{DynamicResource ThemeAccentBrush}" /> |
||||
|
</Style> |
||||
|
</Styles> |
||||
|
|||||
@ -0,0 +1,35 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Transformation; |
||||
|
|
||||
|
namespace Avalonia.Animation.Animators |
||||
|
{ |
||||
|
public class TransformOperationsAnimator : Animator<TransformOperations> |
||||
|
{ |
||||
|
public TransformOperationsAnimator() |
||||
|
{ |
||||
|
Validate = ValidateTransform; |
||||
|
} |
||||
|
|
||||
|
public override TransformOperations Interpolate(double progress, TransformOperations oldValue, TransformOperations newValue) |
||||
|
{ |
||||
|
var oldTransform = EnsureOperations(oldValue); |
||||
|
var newTransform = EnsureOperations(newValue); |
||||
|
|
||||
|
return TransformOperations.Interpolate(oldTransform, newTransform, progress); |
||||
|
} |
||||
|
|
||||
|
internal static TransformOperations EnsureOperations(ITransform value) |
||||
|
{ |
||||
|
return value as TransformOperations ?? TransformOperations.Identity; |
||||
|
} |
||||
|
|
||||
|
private void ValidateTransform(AnimatorKeyFrame kf) |
||||
|
{ |
||||
|
if (!(kf.Value is TransformOperations)) |
||||
|
{ |
||||
|
throw new InvalidOperationException($"All keyframes must be of type {typeof(TransformOperations)}."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
using System; |
||||
|
using System.Reactive.Linq; |
||||
|
using Avalonia.Animation.Animators; |
||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Animation |
||||
|
{ |
||||
|
public class TransformOperationsTransition : Transition<ITransform> |
||||
|
{ |
||||
|
private static readonly TransformOperationsAnimator _operationsAnimator = new TransformOperationsAnimator(); |
||||
|
|
||||
|
public override IObservable<ITransform> DoTransition(IObservable<double> progress, |
||||
|
ITransform oldValue, |
||||
|
ITransform newValue) |
||||
|
{ |
||||
|
var oldTransform = TransformOperationsAnimator.EnsureOperations(oldValue); |
||||
|
var newTransform = TransformOperationsAnimator.EnsureOperations(newValue); |
||||
|
|
||||
|
return progress |
||||
|
.Select(p => |
||||
|
{ |
||||
|
var f = Easing.Ease(p); |
||||
|
|
||||
|
return _operationsAnimator.Interpolate(f, oldTransform, newTransform); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
public interface IMutableTransform : ITransform |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Raised when the transform changes.
|
||||
|
/// </summary>
|
||||
|
event EventHandler Changed; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System.ComponentModel; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
[TypeConverter(typeof(TransformConverter))] |
||||
|
public interface ITransform |
||||
|
{ |
||||
|
Matrix Value { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.Globalization; |
||||
|
using Avalonia.Media.Transformation; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Creates an <see cref="ITransform"/> from a string representation.
|
||||
|
/// </summary>
|
||||
|
public class TransformConverter : TypeConverter |
||||
|
{ |
||||
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
||||
|
{ |
||||
|
return sourceType == typeof(string); |
||||
|
} |
||||
|
|
||||
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
||||
|
{ |
||||
|
return TransformOperations.Parse((string)value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
namespace Avalonia.Media.Transformation |
||||
|
{ |
||||
|
internal static class InterpolationUtilities |
||||
|
{ |
||||
|
public static double InterpolateScalars(double from, double to, double progress) |
||||
|
{ |
||||
|
return from * (1d - progress) + to * progress; |
||||
|
} |
||||
|
|
||||
|
public static Vector InterpolateVectors(Vector from, Vector to, double progress) |
||||
|
{ |
||||
|
var x = InterpolateScalars(from.X, to.X, progress); |
||||
|
var y = InterpolateScalars(from.Y, to.Y, progress); |
||||
|
|
||||
|
return new Vector(x, y); |
||||
|
} |
||||
|
|
||||
|
public static Matrix ComposeTransform(Matrix.Decomposed decomposed) |
||||
|
{ |
||||
|
// According to https://www.w3.org/TR/css-transforms-1/#recomposing-to-a-2d-matrix
|
||||
|
|
||||
|
return Matrix.CreateTranslation(decomposed.Translate) * |
||||
|
Matrix.CreateRotation(decomposed.Angle) * |
||||
|
Matrix.CreateSkew(decomposed.Skew.X, decomposed.Skew.Y) * |
||||
|
Matrix.CreateScale(decomposed.Scale); |
||||
|
} |
||||
|
|
||||
|
public static Matrix.Decomposed InterpolateDecomposedTransforms(ref Matrix.Decomposed from, ref Matrix.Decomposed to, double progres) |
||||
|
{ |
||||
|
Matrix.Decomposed result = default; |
||||
|
|
||||
|
result.Translate = InterpolateVectors(from.Translate, to.Translate, progres); |
||||
|
result.Scale = InterpolateVectors(from.Scale, to.Scale, progres); |
||||
|
result.Skew = InterpolateVectors(from.Skew, to.Skew, progres); |
||||
|
result.Angle = InterpolateScalars(from.Angle, to.Angle, progres); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,230 @@ |
|||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
namespace Avalonia.Media.Transformation |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a single primitive transform (like translation, rotation, scale, etc.).
|
||||
|
/// </summary>
|
||||
|
public struct TransformOperation |
||||
|
{ |
||||
|
public OperationType Type; |
||||
|
public Matrix Matrix; |
||||
|
public DataLayout Data; |
||||
|
|
||||
|
public enum OperationType |
||||
|
{ |
||||
|
Translate, |
||||
|
Rotate, |
||||
|
Scale, |
||||
|
Skew, |
||||
|
Matrix, |
||||
|
Identity |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns whether operation produces the identity matrix.
|
||||
|
/// </summary>
|
||||
|
public bool IsIdentity => Matrix.IsIdentity; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Bakes this operation to a transform matrix.
|
||||
|
/// </summary>
|
||||
|
public void Bake() |
||||
|
{ |
||||
|
Matrix = Matrix.Identity; |
||||
|
|
||||
|
switch (Type) |
||||
|
{ |
||||
|
case OperationType.Translate: |
||||
|
{ |
||||
|
Matrix = Matrix.CreateTranslation(Data.Translate.X, Data.Translate.Y); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case OperationType.Rotate: |
||||
|
{ |
||||
|
Matrix = Matrix.CreateRotation(Data.Rotate.Angle); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case OperationType.Scale: |
||||
|
{ |
||||
|
Matrix = Matrix.CreateScale(Data.Scale.X, Data.Scale.Y); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case OperationType.Skew: |
||||
|
{ |
||||
|
Matrix = Matrix.CreateSkew(Data.Skew.X, Data.Skew.Y); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns new identity transform operation.
|
||||
|
/// </summary>
|
||||
|
public static TransformOperation Identity => |
||||
|
new TransformOperation { Matrix = Matrix.Identity, Type = OperationType.Identity }; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Attempts to interpolate between two transform operations.
|
||||
|
/// </summary>
|
||||
|
/// <param name="from">Source operation.</param>
|
||||
|
/// <param name="to">Target operation.</param>
|
||||
|
/// <param name="progress">Interpolation progress.</param>
|
||||
|
/// <param name="result">Interpolation result that will be filled in when operation was successful.</param>
|
||||
|
/// <remarks>
|
||||
|
/// Based upon https://www.w3.org/TR/css-transforms-1/#interpolation-of-transform-functions.
|
||||
|
/// </remarks>
|
||||
|
public static bool TryInterpolate(TransformOperation? from, TransformOperation? to, double progress, |
||||
|
ref TransformOperation result) |
||||
|
{ |
||||
|
bool fromIdentity = IsOperationIdentity(ref from); |
||||
|
bool toIdentity = IsOperationIdentity(ref to); |
||||
|
|
||||
|
if (fromIdentity && toIdentity) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// ReSharper disable PossibleInvalidOperationException
|
||||
|
TransformOperation fromValue = fromIdentity ? Identity : from.Value; |
||||
|
TransformOperation toValue = toIdentity ? Identity : to.Value; |
||||
|
// ReSharper restore PossibleInvalidOperationException
|
||||
|
|
||||
|
var interpolationType = toIdentity ? fromValue.Type : toValue.Type; |
||||
|
|
||||
|
result.Type = interpolationType; |
||||
|
|
||||
|
switch (interpolationType) |
||||
|
{ |
||||
|
case OperationType.Translate: |
||||
|
{ |
||||
|
double fromX = fromIdentity ? 0 : fromValue.Data.Translate.X; |
||||
|
double fromY = fromIdentity ? 0 : fromValue.Data.Translate.Y; |
||||
|
|
||||
|
double toX = toIdentity ? 0 : toValue.Data.Translate.X; |
||||
|
double toY = toIdentity ? 0 : toValue.Data.Translate.Y; |
||||
|
|
||||
|
result.Data.Translate.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); |
||||
|
result.Data.Translate.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); |
||||
|
|
||||
|
result.Bake(); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case OperationType.Rotate: |
||||
|
{ |
||||
|
double fromAngle = fromIdentity ? 0 : fromValue.Data.Rotate.Angle; |
||||
|
|
||||
|
double toAngle = toIdentity ? 0 : toValue.Data.Rotate.Angle; |
||||
|
|
||||
|
result.Data.Rotate.Angle = InterpolationUtilities.InterpolateScalars(fromAngle, toAngle, progress); |
||||
|
|
||||
|
result.Bake(); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case OperationType.Scale: |
||||
|
{ |
||||
|
double fromX = fromIdentity ? 1 : fromValue.Data.Scale.X; |
||||
|
double fromY = fromIdentity ? 1 : fromValue.Data.Scale.Y; |
||||
|
|
||||
|
double toX = toIdentity ? 1 : toValue.Data.Scale.X; |
||||
|
double toY = toIdentity ? 1 : toValue.Data.Scale.Y; |
||||
|
|
||||
|
result.Data.Scale.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); |
||||
|
result.Data.Scale.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); |
||||
|
|
||||
|
result.Bake(); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case OperationType.Skew: |
||||
|
{ |
||||
|
double fromX = fromIdentity ? 0 : fromValue.Data.Skew.X; |
||||
|
double fromY = fromIdentity ? 0 : fromValue.Data.Skew.Y; |
||||
|
|
||||
|
double toX = toIdentity ? 0 : toValue.Data.Skew.X; |
||||
|
double toY = toIdentity ? 0 : toValue.Data.Skew.Y; |
||||
|
|
||||
|
result.Data.Skew.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); |
||||
|
result.Data.Skew.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); |
||||
|
|
||||
|
result.Bake(); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case OperationType.Matrix: |
||||
|
{ |
||||
|
var fromMatrix = fromIdentity ? Matrix.Identity : fromValue.Matrix; |
||||
|
var toMatrix = toIdentity ? Matrix.Identity : toValue.Matrix; |
||||
|
|
||||
|
if (!Matrix.TryDecomposeTransform(fromMatrix, out Matrix.Decomposed fromDecomposed) || |
||||
|
!Matrix.TryDecomposeTransform(toMatrix, out Matrix.Decomposed toDecomposed)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
var interpolated = |
||||
|
InterpolationUtilities.InterpolateDecomposedTransforms( |
||||
|
ref fromDecomposed, ref toDecomposed, |
||||
|
progress); |
||||
|
|
||||
|
result.Matrix = InterpolationUtilities.ComposeTransform(interpolated); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case OperationType.Identity: |
||||
|
{ |
||||
|
// Do nothing.
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private static bool IsOperationIdentity(ref TransformOperation? operation) |
||||
|
{ |
||||
|
return !operation.HasValue || operation.Value.IsIdentity; |
||||
|
} |
||||
|
|
||||
|
[StructLayout(LayoutKind.Explicit)] |
||||
|
public struct DataLayout |
||||
|
{ |
||||
|
[FieldOffset(0)] public SkewLayout Skew; |
||||
|
|
||||
|
[FieldOffset(0)] public ScaleLayout Scale; |
||||
|
|
||||
|
[FieldOffset(0)] public TranslateLayout Translate; |
||||
|
|
||||
|
[FieldOffset(0)] public RotateLayout Rotate; |
||||
|
|
||||
|
public struct SkewLayout |
||||
|
{ |
||||
|
public double X; |
||||
|
public double Y; |
||||
|
} |
||||
|
|
||||
|
public struct ScaleLayout |
||||
|
{ |
||||
|
public double X; |
||||
|
public double Y; |
||||
|
} |
||||
|
|
||||
|
public struct TranslateLayout |
||||
|
{ |
||||
|
public double X; |
||||
|
public double Y; |
||||
|
} |
||||
|
|
||||
|
public struct RotateLayout |
||||
|
{ |
||||
|
public double Angle; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,258 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Media.Transformation |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Contains a list of <see cref="TransformOperation"/> that represent primitive transforms that will be
|
||||
|
/// applied in declared order.
|
||||
|
/// </summary>
|
||||
|
public sealed class TransformOperations : ITransform |
||||
|
{ |
||||
|
public static TransformOperations Identity { get; } = new TransformOperations(new List<TransformOperation>()); |
||||
|
|
||||
|
private readonly List<TransformOperation> _operations; |
||||
|
|
||||
|
private TransformOperations(List<TransformOperation> operations) |
||||
|
{ |
||||
|
_operations = operations ?? throw new ArgumentNullException(nameof(operations)); |
||||
|
|
||||
|
IsIdentity = CheckIsIdentity(); |
||||
|
|
||||
|
Value = ApplyTransforms(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns whether all operations combined together produce the identity matrix.
|
||||
|
/// </summary>
|
||||
|
public bool IsIdentity { get; } |
||||
|
|
||||
|
public IReadOnlyList<TransformOperation> Operations => _operations; |
||||
|
|
||||
|
public Matrix Value { get; } |
||||
|
|
||||
|
public static TransformOperations Parse(string s) |
||||
|
{ |
||||
|
return TransformParser.Parse(s); |
||||
|
} |
||||
|
|
||||
|
public static Builder CreateBuilder(int capacity) |
||||
|
{ |
||||
|
return new Builder(capacity); |
||||
|
} |
||||
|
|
||||
|
public static TransformOperations Interpolate(TransformOperations from, TransformOperations to, double progress) |
||||
|
{ |
||||
|
TransformOperations result = Identity; |
||||
|
|
||||
|
if (!TryInterpolate(from, to, progress, ref result)) |
||||
|
{ |
||||
|
// If the matrices cannot be interpolated, fallback to discrete animation logic.
|
||||
|
// See https://drafts.csswg.org/css-transforms/#matrix-interpolation
|
||||
|
result = progress < 0.5 ? from : to; |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private Matrix ApplyTransforms(int startOffset = 0) |
||||
|
{ |
||||
|
Matrix matrix = Matrix.Identity; |
||||
|
|
||||
|
for (var i = startOffset; i < _operations.Count; i++) |
||||
|
{ |
||||
|
TransformOperation operation = _operations[i]; |
||||
|
matrix *= operation.Matrix; |
||||
|
} |
||||
|
|
||||
|
return matrix; |
||||
|
} |
||||
|
|
||||
|
private bool CheckIsIdentity() |
||||
|
{ |
||||
|
foreach (TransformOperation operation in _operations) |
||||
|
{ |
||||
|
if (!operation.IsIdentity) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private static bool TryInterpolate(TransformOperations from, TransformOperations to, double progress, ref TransformOperations result) |
||||
|
{ |
||||
|
bool fromIdentity = from.IsIdentity; |
||||
|
bool toIdentity = to.IsIdentity; |
||||
|
|
||||
|
if (fromIdentity && toIdentity) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
int matchingPrefixLength = ComputeMatchingPrefixLength(from, to); |
||||
|
int fromSize = fromIdentity ? 0 : from._operations.Count; |
||||
|
int toSize = toIdentity ? 0 : to._operations.Count; |
||||
|
int numOperations = Math.Max(fromSize, toSize); |
||||
|
|
||||
|
var builder = new Builder(matchingPrefixLength); |
||||
|
|
||||
|
for (int i = 0; i < matchingPrefixLength; i++) |
||||
|
{ |
||||
|
TransformOperation interpolated = new TransformOperation |
||||
|
{ |
||||
|
Type = TransformOperation.OperationType.Identity |
||||
|
}; |
||||
|
|
||||
|
if (!TransformOperation.TryInterpolate( |
||||
|
i >= fromSize ? default(TransformOperation?) : from._operations[i], |
||||
|
i >= toSize ? default(TransformOperation?) : to._operations[i], |
||||
|
progress, |
||||
|
ref interpolated)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
builder.Append(interpolated); |
||||
|
} |
||||
|
|
||||
|
if (matchingPrefixLength < numOperations) |
||||
|
{ |
||||
|
if (!ComputeDecomposedTransform(from, matchingPrefixLength, out Matrix.Decomposed fromDecomposed) || |
||||
|
!ComputeDecomposedTransform(to, matchingPrefixLength, out Matrix.Decomposed toDecomposed)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
var transform = InterpolationUtilities.InterpolateDecomposedTransforms(ref fromDecomposed, ref toDecomposed, progress); |
||||
|
|
||||
|
builder.AppendMatrix(InterpolationUtilities.ComposeTransform(transform)); |
||||
|
} |
||||
|
|
||||
|
result = builder.Build(); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private static bool ComputeDecomposedTransform(TransformOperations operations, int startOffset, out Matrix.Decomposed decomposed) |
||||
|
{ |
||||
|
Matrix transform = operations.ApplyTransforms(startOffset); |
||||
|
|
||||
|
if (!Matrix.TryDecomposeTransform(transform, out decomposed)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private static int ComputeMatchingPrefixLength(TransformOperations from, TransformOperations to) |
||||
|
{ |
||||
|
int numOperations = Math.Min(from._operations.Count, to._operations.Count); |
||||
|
|
||||
|
for (int i = 0; i < numOperations; i++) |
||||
|
{ |
||||
|
if (from._operations[i].Type != to._operations[i].Type) |
||||
|
{ |
||||
|
return i; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// If the operations match to the length of the shorter list, then pad its
|
||||
|
// length with the matching identity operations.
|
||||
|
// https://drafts.csswg.org/css-transforms/#transform-function-lists
|
||||
|
return Math.Max(from._operations.Count, to._operations.Count); |
||||
|
} |
||||
|
|
||||
|
public readonly struct Builder |
||||
|
{ |
||||
|
private readonly List<TransformOperation> _operations; |
||||
|
|
||||
|
public Builder(int capacity) |
||||
|
{ |
||||
|
_operations = new List<TransformOperation>(capacity); |
||||
|
} |
||||
|
|
||||
|
public void AppendTranslate(double x, double y) |
||||
|
{ |
||||
|
var toAdd = new TransformOperation(); |
||||
|
|
||||
|
toAdd.Type = TransformOperation.OperationType.Translate; |
||||
|
toAdd.Data.Translate.X = x; |
||||
|
toAdd.Data.Translate.Y = y; |
||||
|
|
||||
|
toAdd.Bake(); |
||||
|
|
||||
|
_operations.Add(toAdd); |
||||
|
} |
||||
|
|
||||
|
public void AppendRotate(double angle) |
||||
|
{ |
||||
|
var toAdd = new TransformOperation(); |
||||
|
|
||||
|
toAdd.Type = TransformOperation.OperationType.Rotate; |
||||
|
toAdd.Data.Rotate.Angle = angle; |
||||
|
|
||||
|
toAdd.Bake(); |
||||
|
|
||||
|
_operations.Add(toAdd); |
||||
|
} |
||||
|
|
||||
|
public void AppendScale(double x, double y) |
||||
|
{ |
||||
|
var toAdd = new TransformOperation(); |
||||
|
|
||||
|
toAdd.Type = TransformOperation.OperationType.Scale; |
||||
|
toAdd.Data.Scale.X = x; |
||||
|
toAdd.Data.Scale.Y = y; |
||||
|
|
||||
|
toAdd.Bake(); |
||||
|
|
||||
|
_operations.Add(toAdd); |
||||
|
} |
||||
|
|
||||
|
public void AppendSkew(double x, double y) |
||||
|
{ |
||||
|
var toAdd = new TransformOperation(); |
||||
|
|
||||
|
toAdd.Type = TransformOperation.OperationType.Skew; |
||||
|
toAdd.Data.Skew.X = x; |
||||
|
toAdd.Data.Skew.Y = y; |
||||
|
|
||||
|
toAdd.Bake(); |
||||
|
|
||||
|
_operations.Add(toAdd); |
||||
|
} |
||||
|
|
||||
|
public void AppendMatrix(Matrix matrix) |
||||
|
{ |
||||
|
var toAdd = new TransformOperation(); |
||||
|
|
||||
|
toAdd.Type = TransformOperation.OperationType.Matrix; |
||||
|
toAdd.Matrix = matrix; |
||||
|
|
||||
|
_operations.Add(toAdd); |
||||
|
} |
||||
|
|
||||
|
public void AppendIdentity() |
||||
|
{ |
||||
|
var toAdd = new TransformOperation(); |
||||
|
|
||||
|
toAdd.Type = TransformOperation.OperationType.Identity; |
||||
|
|
||||
|
_operations.Add(toAdd); |
||||
|
} |
||||
|
|
||||
|
public void Append(TransformOperation toAdd) |
||||
|
{ |
||||
|
_operations.Add(toAdd); |
||||
|
} |
||||
|
|
||||
|
public TransformOperations Build() |
||||
|
{ |
||||
|
return new TransformOperations(_operations); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,444 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media.Transformation |
||||
|
{ |
||||
|
public static class TransformParser |
||||
|
{ |
||||
|
private static readonly (string, TransformFunction)[] s_functionMapping = |
||||
|
{ |
||||
|
("translate", TransformFunction.Translate), |
||||
|
("translateX", TransformFunction.TranslateX), |
||||
|
("translateY", TransformFunction.TranslateY), |
||||
|
("scale", TransformFunction.Scale), |
||||
|
("scaleX", TransformFunction.ScaleX), |
||||
|
("scaleY", TransformFunction.ScaleY), |
||||
|
("skew", TransformFunction.Skew), |
||||
|
("skewX", TransformFunction.SkewX), |
||||
|
("skewY", TransformFunction.SkewY), |
||||
|
("rotate", TransformFunction.Rotate), |
||||
|
("matrix", TransformFunction.Matrix) |
||||
|
}; |
||||
|
|
||||
|
private static readonly (string, Unit)[] s_unitMapping = |
||||
|
{ |
||||
|
("deg", Unit.Degree), |
||||
|
("grad", Unit.Gradian), |
||||
|
("rad", Unit.Radian), |
||||
|
("turn", Unit.Turn), |
||||
|
("px", Unit.Pixel) |
||||
|
}; |
||||
|
|
||||
|
public static TransformOperations Parse(string s) |
||||
|
{ |
||||
|
void ThrowInvalidFormat() |
||||
|
{ |
||||
|
throw new FormatException($"Invalid transform string: '{s}'."); |
||||
|
} |
||||
|
|
||||
|
if (string.IsNullOrEmpty(s)) |
||||
|
{ |
||||
|
throw new ArgumentException(nameof(s)); |
||||
|
} |
||||
|
|
||||
|
var span = s.AsSpan().Trim(); |
||||
|
|
||||
|
if (span.Equals("none".AsSpan(), StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
return TransformOperations.Identity; |
||||
|
} |
||||
|
|
||||
|
var builder = TransformOperations.CreateBuilder(0); |
||||
|
|
||||
|
while (true) |
||||
|
{ |
||||
|
var beginIndex = span.IndexOf('('); |
||||
|
var endIndex = span.IndexOf(')'); |
||||
|
|
||||
|
if (beginIndex == -1 || endIndex == -1) |
||||
|
{ |
||||
|
ThrowInvalidFormat(); |
||||
|
} |
||||
|
|
||||
|
var namePart = span.Slice(0, beginIndex).Trim(); |
||||
|
|
||||
|
var function = ParseTransformFunction(in namePart); |
||||
|
|
||||
|
if (function == TransformFunction.Invalid) |
||||
|
{ |
||||
|
ThrowInvalidFormat(); |
||||
|
} |
||||
|
|
||||
|
var valuePart = span.Slice(beginIndex + 1, endIndex - beginIndex - 1).Trim(); |
||||
|
|
||||
|
ParseFunction(in valuePart, function, in builder); |
||||
|
|
||||
|
span = span.Slice(endIndex + 1); |
||||
|
|
||||
|
if (span.IsWhiteSpace()) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return builder.Build(); |
||||
|
} |
||||
|
|
||||
|
private static void ParseFunction( |
||||
|
in ReadOnlySpan<char> functionPart, |
||||
|
TransformFunction function, |
||||
|
in TransformOperations.Builder builder) |
||||
|
{ |
||||
|
static UnitValue ParseValue(ReadOnlySpan<char> part) |
||||
|
{ |
||||
|
int unitIndex = -1; |
||||
|
|
||||
|
for (int i = 0; i < part.Length; i++) |
||||
|
{ |
||||
|
char c = part[i]; |
||||
|
|
||||
|
if (char.IsDigit(c) || c == '-' || c == '.') |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
unitIndex = i; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
Unit unit = Unit.None; |
||||
|
|
||||
|
if (unitIndex != -1) |
||||
|
{ |
||||
|
var unitPart = part.Slice(unitIndex, part.Length - unitIndex); |
||||
|
|
||||
|
unit = ParseUnit(unitPart); |
||||
|
|
||||
|
part = part.Slice(0, unitIndex); |
||||
|
} |
||||
|
|
||||
|
var value = double.Parse(part.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture); |
||||
|
|
||||
|
return new UnitValue(unit, value); |
||||
|
} |
||||
|
|
||||
|
static int ParseValuePair( |
||||
|
in ReadOnlySpan<char> part, |
||||
|
ref UnitValue leftValue, |
||||
|
ref UnitValue rightValue) |
||||
|
{ |
||||
|
var commaIndex = part.IndexOf(','); |
||||
|
|
||||
|
if (commaIndex != -1) |
||||
|
{ |
||||
|
var leftPart = part.Slice(0, commaIndex).Trim(); |
||||
|
var rightPart = part.Slice(commaIndex + 1, part.Length - commaIndex - 1).Trim(); |
||||
|
|
||||
|
leftValue = ParseValue(leftPart); |
||||
|
rightValue = ParseValue(rightPart); |
||||
|
|
||||
|
return 2; |
||||
|
} |
||||
|
|
||||
|
leftValue = ParseValue(part); |
||||
|
|
||||
|
return 1; |
||||
|
} |
||||
|
|
||||
|
static int ParseCommaDelimitedValues(ReadOnlySpan<char> part, in Span<UnitValue> outValues) |
||||
|
{ |
||||
|
int valueIndex = 0; |
||||
|
|
||||
|
while (true) |
||||
|
{ |
||||
|
if (valueIndex >= outValues.Length) |
||||
|
{ |
||||
|
throw new FormatException("Too many provided values."); |
||||
|
} |
||||
|
|
||||
|
var commaIndex = part.IndexOf(','); |
||||
|
|
||||
|
if (commaIndex == -1) |
||||
|
{ |
||||
|
if (!part.IsWhiteSpace()) |
||||
|
{ |
||||
|
outValues[valueIndex++] = ParseValue(part); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
var valuePart = part.Slice(0, commaIndex).Trim(); |
||||
|
|
||||
|
outValues[valueIndex++] = ParseValue(valuePart); |
||||
|
|
||||
|
part = part.Slice(commaIndex + 1, part.Length - commaIndex - 1); |
||||
|
} |
||||
|
|
||||
|
return valueIndex; |
||||
|
} |
||||
|
|
||||
|
switch (function) |
||||
|
{ |
||||
|
case TransformFunction.Scale: |
||||
|
case TransformFunction.ScaleX: |
||||
|
case TransformFunction.ScaleY: |
||||
|
{ |
||||
|
var scaleX = UnitValue.One; |
||||
|
var scaleY = UnitValue.One; |
||||
|
|
||||
|
int count = ParseValuePair(functionPart, ref scaleX, ref scaleY); |
||||
|
|
||||
|
if (count != 1 && (function == TransformFunction.ScaleX || function == TransformFunction.ScaleY)) |
||||
|
{ |
||||
|
ThrowFormatInvalidValueCount(function, 1); |
||||
|
} |
||||
|
|
||||
|
VerifyZeroOrUnit(function, in scaleX, Unit.None); |
||||
|
VerifyZeroOrUnit(function, in scaleY, Unit.None); |
||||
|
|
||||
|
if (function == TransformFunction.ScaleY) |
||||
|
{ |
||||
|
scaleY = scaleX; |
||||
|
scaleX = UnitValue.One; |
||||
|
} |
||||
|
else if (function == TransformFunction.Scale && count == 1) |
||||
|
{ |
||||
|
scaleY = scaleX; |
||||
|
} |
||||
|
|
||||
|
builder.AppendScale(scaleX.Value, scaleY.Value); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case TransformFunction.Skew: |
||||
|
case TransformFunction.SkewX: |
||||
|
case TransformFunction.SkewY: |
||||
|
{ |
||||
|
var skewX = UnitValue.Zero; |
||||
|
var skewY = UnitValue.Zero; |
||||
|
|
||||
|
int count = ParseValuePair(functionPart, ref skewX, ref skewY); |
||||
|
|
||||
|
if (count != 1 && (function == TransformFunction.SkewX || function == TransformFunction.SkewY)) |
||||
|
{ |
||||
|
ThrowFormatInvalidValueCount(function, 1); |
||||
|
} |
||||
|
|
||||
|
VerifyZeroOrAngle(function, in skewX); |
||||
|
VerifyZeroOrAngle(function, in skewY); |
||||
|
|
||||
|
if (function == TransformFunction.SkewY) |
||||
|
{ |
||||
|
skewY = skewX; |
||||
|
skewX = UnitValue.Zero; |
||||
|
} |
||||
|
|
||||
|
builder.AppendSkew(ToRadians(in skewX), ToRadians(in skewY)); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case TransformFunction.Rotate: |
||||
|
{ |
||||
|
var angle = UnitValue.Zero; |
||||
|
UnitValue _ = default; |
||||
|
|
||||
|
int count = ParseValuePair(functionPart, ref angle, ref _); |
||||
|
|
||||
|
if (count != 1) |
||||
|
{ |
||||
|
ThrowFormatInvalidValueCount(function, 1); |
||||
|
} |
||||
|
|
||||
|
VerifyZeroOrAngle(function, in angle); |
||||
|
|
||||
|
builder.AppendRotate(ToRadians(in angle)); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case TransformFunction.Translate: |
||||
|
case TransformFunction.TranslateX: |
||||
|
case TransformFunction.TranslateY: |
||||
|
{ |
||||
|
var translateX = UnitValue.Zero; |
||||
|
var translateY = UnitValue.Zero; |
||||
|
|
||||
|
int count = ParseValuePair(functionPart, ref translateX, ref translateY); |
||||
|
|
||||
|
if (count != 1 && (function == TransformFunction.TranslateX || function == TransformFunction.TranslateY)) |
||||
|
{ |
||||
|
ThrowFormatInvalidValueCount(function, 1); |
||||
|
} |
||||
|
|
||||
|
VerifyZeroOrUnit(function, in translateX, Unit.Pixel); |
||||
|
VerifyZeroOrUnit(function, in translateY, Unit.Pixel); |
||||
|
|
||||
|
if (function == TransformFunction.TranslateY) |
||||
|
{ |
||||
|
translateY = translateX; |
||||
|
translateX = UnitValue.Zero; |
||||
|
} |
||||
|
|
||||
|
builder.AppendTranslate(translateX.Value, translateY.Value); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case TransformFunction.Matrix: |
||||
|
{ |
||||
|
Span<UnitValue> values = stackalloc UnitValue[6]; |
||||
|
|
||||
|
int count = ParseCommaDelimitedValues(functionPart, in values); |
||||
|
|
||||
|
if (count != 6) |
||||
|
{ |
||||
|
ThrowFormatInvalidValueCount(function, 6); |
||||
|
} |
||||
|
|
||||
|
foreach (UnitValue value in values) |
||||
|
{ |
||||
|
VerifyZeroOrUnit(function, value, Unit.None); |
||||
|
} |
||||
|
|
||||
|
var matrix = new Matrix( |
||||
|
values[0].Value, |
||||
|
values[1].Value, |
||||
|
values[2].Value, |
||||
|
values[3].Value, |
||||
|
values[4].Value, |
||||
|
values[5].Value); |
||||
|
|
||||
|
builder.AppendMatrix(matrix); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void VerifyZeroOrUnit(TransformFunction function, in UnitValue value, Unit unit) |
||||
|
{ |
||||
|
bool isZero = value.Unit == Unit.None && value.Value == 0d; |
||||
|
|
||||
|
if (!isZero && value.Unit != unit) |
||||
|
{ |
||||
|
ThrowFormatInvalidValue(function, in value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void VerifyZeroOrAngle(TransformFunction function, in UnitValue value) |
||||
|
{ |
||||
|
if (value.Value != 0d && !IsAngleUnit(value.Unit)) |
||||
|
{ |
||||
|
ThrowFormatInvalidValue(function, in value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static bool IsAngleUnit(Unit unit) |
||||
|
{ |
||||
|
switch (unit) |
||||
|
{ |
||||
|
case Unit.Radian: |
||||
|
case Unit.Gradian: |
||||
|
case Unit.Degree: |
||||
|
case Unit.Turn: |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private static void ThrowFormatInvalidValue(TransformFunction function, in UnitValue value) |
||||
|
{ |
||||
|
var unitString = value.Unit == Unit.None ? string.Empty : value.Unit.ToString(); |
||||
|
|
||||
|
throw new FormatException($"Invalid value {value.Value} {unitString} for {function}"); |
||||
|
} |
||||
|
|
||||
|
private static void ThrowFormatInvalidValueCount(TransformFunction function, int count) |
||||
|
{ |
||||
|
throw new FormatException($"Invalid format. {function} expects {count} value(s)."); |
||||
|
} |
||||
|
|
||||
|
private static Unit ParseUnit(in ReadOnlySpan<char> part) |
||||
|
{ |
||||
|
foreach (var (name, unit) in s_unitMapping) |
||||
|
{ |
||||
|
if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
return unit; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
throw new FormatException($"Invalid unit: {part.ToString()}"); |
||||
|
} |
||||
|
|
||||
|
private static TransformFunction ParseTransformFunction(in ReadOnlySpan<char> part) |
||||
|
{ |
||||
|
foreach (var (name, transformFunction) in s_functionMapping) |
||||
|
{ |
||||
|
if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
return transformFunction; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return TransformFunction.Invalid; |
||||
|
} |
||||
|
|
||||
|
private static double ToRadians(in UnitValue value) |
||||
|
{ |
||||
|
return value.Unit switch |
||||
|
{ |
||||
|
Unit.Radian => value.Value, |
||||
|
Unit.Gradian => MathUtilities.Grad2Rad(value.Value), |
||||
|
Unit.Degree => MathUtilities.Deg2Rad(value.Value), |
||||
|
Unit.Turn => MathUtilities.Turn2Rad(value.Value), |
||||
|
_ => value.Value |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private enum Unit |
||||
|
{ |
||||
|
None, |
||||
|
Pixel, |
||||
|
Radian, |
||||
|
Gradian, |
||||
|
Degree, |
||||
|
Turn |
||||
|
} |
||||
|
|
||||
|
private readonly struct UnitValue |
||||
|
{ |
||||
|
public readonly Unit Unit; |
||||
|
public readonly double Value; |
||||
|
|
||||
|
public UnitValue(Unit unit, double value) |
||||
|
{ |
||||
|
Unit = unit; |
||||
|
Value = value; |
||||
|
} |
||||
|
|
||||
|
public static UnitValue Zero => new UnitValue(Unit.None, 0); |
||||
|
|
||||
|
public static UnitValue One => new UnitValue(Unit.None, 1); |
||||
|
} |
||||
|
|
||||
|
private enum TransformFunction |
||||
|
{ |
||||
|
Invalid, |
||||
|
Translate, |
||||
|
TranslateX, |
||||
|
TranslateY, |
||||
|
Scale, |
||||
|
ScaleX, |
||||
|
ScaleY, |
||||
|
Skew, |
||||
|
SkewX, |
||||
|
SkewY, |
||||
|
Rotate, |
||||
|
Matrix |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue