diff --git a/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf
new file mode 100644
index 0000000000..f6986468bd
Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf differ
diff --git a/samples/ControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf
new file mode 100644
index 0000000000..5c00b64faf
Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf differ
diff --git a/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf
new file mode 100644
index 0000000000..82e8762011
Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf differ
diff --git a/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf
new file mode 100644
index 0000000000..278ad8aa0a
Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf differ
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index b8a8479a49..c37002ef6a 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -11,6 +11,12 @@
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 377871f658..87cb5e9c5c 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -2,9 +2,9 @@
xmlns:pages="clr-namespace:ControlCatalog.Pages"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-
+
-
+
diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml b/samples/ControlCatalog/Pages/CarouselPage.xaml
index 4a5b2cea26..3468b71fd8 100644
--- a/samples/ControlCatalog/Pages/CarouselPage.xaml
+++ b/samples/ControlCatalog/Pages/CarouselPage.xaml
@@ -8,9 +8,9 @@
-
+
-
+
diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage.xaml.cs
index 272290e679..5e24bfb58f 100644
--- a/samples/ControlCatalog/Pages/CarouselPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CarouselPage.xaml.cs
@@ -37,13 +37,13 @@ namespace ControlCatalog.Pages
switch (_transition.SelectedIndex)
{
case 0:
- _carousel.Transition = null;
+ _carousel.PageTransition = null;
break;
case 1:
- _carousel.Transition = new PageSlide(TimeSpan.FromSeconds(0.25), _orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical);
+ _carousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), _orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical);
break;
case 2:
- _carousel.Transition = new CrossFade(TimeSpan.FromSeconds(0.25));
+ _carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25));
break;
}
}
diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml
index db1c9cb69d..fb0d8fb7b1 100644
--- a/samples/ControlCatalog/Pages/TextBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml
@@ -31,6 +31,12 @@
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml
index 7fcd828ba3..e5baf9e942 100644
--- a/samples/ControlCatalog/SideBar.xaml
+++ b/samples/ControlCatalog/SideBar.xaml
@@ -1,4 +1,5 @@
-
+
+
+
diff --git a/samples/RenderTest/App.xaml.cs b/samples/RenderTest/App.xaml.cs
index 4492e5ae4c..fd2b940f6a 100644
--- a/samples/RenderTest/App.xaml.cs
+++ b/samples/RenderTest/App.xaml.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia;
+using Avalonia.Logging.Serilog;
using Avalonia.Markup.Xaml;
namespace RenderTest
@@ -12,5 +13,17 @@ namespace RenderTest
{
AvaloniaXamlLoader.Load(this);
}
+
+ // TODO: Make this work with GTK/Skia/Cairo depending on command-line args
+ // again.
+ static void Main(string[] args) => BuildAvaloniaApp().Start();
+
+ // App configuration, used by the entry point and previewer
+ static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .UseReactiveUI()
+ .LogToDebug();
+
}
}
diff --git a/samples/RenderTest/MainWindow.xaml b/samples/RenderTest/MainWindow.xaml
index ce8fd3a43e..da49054b77 100644
--- a/samples/RenderTest/MainWindow.xaml
+++ b/samples/RenderTest/MainWindow.xaml
@@ -24,9 +24,9 @@
-
+
-
+
diff --git a/samples/RenderTest/Pages/AnimationsPage.xaml b/samples/RenderTest/Pages/AnimationsPage.xaml
index 6eb7067245..415c08728f 100644
--- a/samples/RenderTest/Pages/AnimationsPage.xaml
+++ b/samples/RenderTest/Pages/AnimationsPage.xaml
@@ -1,2 +1,123 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hover to activate Transform Keyframe Animations.
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/RenderTest/Pages/AnimationsPage.xaml.cs b/samples/RenderTest/Pages/AnimationsPage.xaml.cs
index 991896c3b5..2623721393 100644
--- a/samples/RenderTest/Pages/AnimationsPage.xaml.cs
+++ b/samples/RenderTest/Pages/AnimationsPage.xaml.cs
@@ -7,6 +7,7 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
+using RenderTest.ViewModels;
namespace RenderTest.Pages
{
@@ -14,77 +15,13 @@ namespace RenderTest.Pages
{
public AnimationsPage()
{
- this.InitializeComponent();
- this.CreateAnimations();
+ InitializeComponent();
+ this.DataContext = new AnimationsPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
-
- private void CreateAnimations()
- {
- const int Count = 100;
- var panel = new WrapPanel();
-
- for (var i = 0; i < Count; ++i)
- {
- Ellipse ellipse;
- var element = new Panel
- {
- Children =
- {
- (ellipse = new Ellipse
- {
- Name = $"ellipse{i}",
- Width = 100,
- Height = 100,
- Fill = Brushes.Blue,
- }),
- new Path
- {
- Data = StreamGeometry.Parse(
- "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"),
- Fill = Brushes.Green,
- HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
- VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
- RenderTransform = new ScaleTransform(2, 2),
- }
- },
- Margin = new Thickness(4),
- RenderTransform = new ScaleTransform(),
- };
-
- var start = Animate.Stopwatch.Elapsed;
- var index = i % (Count / 2);
- var degrees = Animate.Timer
- .Select(x => (x - start).TotalSeconds)
- .Where(x => (x % Count) >= index && (x % Count) < index + 1)
- .Select(x => (x % 1) / 1);
-
- element.RenderTransform.Bind(
- ScaleTransform.ScaleXProperty,
- degrees,
- BindingPriority.Animation);
-
- ellipse.PointerEnter += Ellipse_PointerEnter;
- ellipse.PointerLeave += Ellipse_PointerLeave;
-
- panel.Children.Add(element);
- }
-
- Content = panel;
- }
-
- private void Ellipse_PointerEnter(object sender, PointerEventArgs e)
- {
- ((Ellipse)sender).Fill = Brushes.Red;
- }
-
- private void Ellipse_PointerLeave(object sender, PointerEventArgs e)
- {
- ((Ellipse)sender).Fill = Brushes.Blue;
- }
}
}
diff --git a/samples/RenderTest/Pages/ClippingPage.xaml b/samples/RenderTest/Pages/ClippingPage.xaml
index 238e3449de..434468888a 100644
--- a/samples/RenderTest/Pages/ClippingPage.xaml
+++ b/samples/RenderTest/Pages/ClippingPage.xaml
@@ -1,13 +1,52 @@
-
+
-
+
+
+
+
+
+
+ Clip="M 58.625 0.07421875
+ C 50.305778 0.26687364 42.411858 7.0346526 41.806641 15.595703
+ C 42.446442 22.063923 39.707425 13.710754 36.982422 12.683594
+ C 29.348395 6.1821635 16.419398 8.4359222 11.480469 17.195312
+ C 6.0935256 25.476803 9.8118851 37.71125 18.8125 41.6875
+ C 9.1554771 40.62945 -0.070876925 49.146842 0.21679688 58.857422
+ C 0.21545578 60.872512 0.56758794 62.88911 1.2617188 64.78125
+ C 4.3821886 74.16708 16.298268 78.921772 25.03125 74.326172
+ C 28.266843 72.062552 26.298191 74.214838 25.414062 76.398438
+ C 21.407348 85.589198 27.295992 97.294293 37.097656 99.501953
+ C 46.864883 102.3541 57.82177 94.726518 58.539062 84.580078
+ C 58.142158 79.498998 59.307538 83.392694 61.207031 85.433594
+ C 67.532324 93.056874 80.440232 93.192029 86.882812 85.630859
+ C 93.836392 78.456939 92.396838 65.538666 84.115234 60.009766
+ C 79.783641 57.904836 83.569793 58.802369 86.375 58.193359
+ C 96.383335 56.457569 102.87506 44.824101 99.083984 35.394531
+ C 95.963498 26.008711 84.047451 21.254079 75.314453 25.849609
+ C 72.078834 28.113269 74.047517 25.960974 74.931641 23.777344
+ C 78.93827 14.586564 73.049722 2.8815081 63.248047 0.67382812
+ C 61.721916 0.22817968 60.165597 0.038541919 58.625 0.07421875 z ">
+
+
+
Apply Geometry Clip
diff --git a/samples/RenderTest/Pages/ClippingPage.xaml.cs b/samples/RenderTest/Pages/ClippingPage.xaml.cs
index b416f12943..2a79076d4c 100644
--- a/samples/RenderTest/Pages/ClippingPage.xaml.cs
+++ b/samples/RenderTest/Pages/ClippingPage.xaml.cs
@@ -16,7 +16,6 @@ namespace RenderTest.Pages
public ClippingPage()
{
InitializeComponent();
- CreateAnimations();
WireUpCheckbox();
}
@@ -25,18 +24,6 @@ namespace RenderTest.Pages
AvaloniaXamlLoader.Load(this);
}
- private void CreateAnimations()
- {
- var clipped = this.FindControl("clipChild");
- var degrees = Animate.Timer.Select(x => x.TotalMilliseconds / 5);
- clipped.RenderTransform = new RotateTransform();
- clipped.RenderTransform.Bind(RotateTransform.AngleProperty, degrees, BindingPriority.Animation);
- clipped.Bind(
- Border.BackgroundProperty,
- clipped.GetObservable(Control.IsPointerOverProperty)
- .Select(x => x ? Brushes.Crimson : AvaloniaProperty.UnsetValue));
- }
-
private void WireUpCheckbox()
{
var useMask = this.FindControl("useMask");
diff --git a/samples/RenderTest/Program.cs b/samples/RenderTest/Program.cs
deleted file mode 100644
index a1a2b8c410..0000000000
--- a/samples/RenderTest/Program.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-using System.Linq;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Logging.Serilog;
-using Avalonia.Platform;
-using Serilog;
-
-namespace RenderTest
-{
- internal class Program
- {
- static void Main(string[] args)
- {
- // TODO: Make this work with GTK/Skia/Cairo depending on command-line args
- // again.
- AppBuilder.Configure()
- .UsePlatformDetect()
- .UseReactiveUI()
- .LogToDebug()
- .Start();
- }
- }
-}
diff --git a/samples/RenderTest/SideBar.xaml b/samples/RenderTest/SideBar.xaml
index 7fcd828ba3..29e5d854f9 100644
--- a/samples/RenderTest/SideBar.xaml
+++ b/samples/RenderTest/SideBar.xaml
@@ -1,4 +1,6 @@
-
+
+
+
diff --git a/samples/RenderTest/ViewModels/AnimationsPageViewModel.cs b/samples/RenderTest/ViewModels/AnimationsPageViewModel.cs
new file mode 100644
index 0000000000..17eee547a1
--- /dev/null
+++ b/samples/RenderTest/ViewModels/AnimationsPageViewModel.cs
@@ -0,0 +1,40 @@
+using System;
+using ReactiveUI;
+using Avalonia.Animation;
+
+namespace RenderTest.ViewModels
+{
+ public class AnimationsPageViewModel : ReactiveObject
+ {
+ private string _playStateText = "Pause all animations";
+
+ public AnimationsPageViewModel()
+ {
+ ToggleGlobalPlayState = ReactiveCommand.Create(()=>TogglePlayState());
+ }
+
+ void TogglePlayState()
+ {
+ switch (Timing.GetGlobalPlayState())
+ {
+ case PlayState.Run:
+ PlayStateText = "Resume all animations";
+ Timing.SetGlobalPlayState(PlayState.Pause);
+ break;
+
+ case PlayState.Pause:
+ PlayStateText = "Pause all animations";
+ Timing.SetGlobalPlayState(PlayState.Run);
+ break;
+ }
+ }
+
+ public string PlayStateText
+ {
+ get { return _playStateText; }
+ set { this.RaiseAndSetIfChanged(ref _playStateText, value); }
+ }
+
+ public ReactiveCommand ToggleGlobalPlayState { get; }
+ }
+}
diff --git a/samples/RenderTest/ViewModels/MainWindowViewModel.cs b/samples/RenderTest/ViewModels/MainWindowViewModel.cs
index b2fa2e8b7a..02a2abeb89 100644
--- a/samples/RenderTest/ViewModels/MainWindowViewModel.cs
+++ b/samples/RenderTest/ViewModels/MainWindowViewModel.cs
@@ -5,7 +5,7 @@ namespace RenderTest.ViewModels
{
public class MainWindowViewModel : ReactiveObject
{
- private bool drawDirtyRects = true;
+ private bool drawDirtyRects = false;
private bool drawFps = true;
public MainWindowViewModel()
diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs
index 0231ffa0be..a27d996301 100644
--- a/src/Avalonia.Animation/Animatable.cs
+++ b/src/Avalonia.Animation/Animatable.cs
@@ -3,6 +3,13 @@
using System.Linq;
using Avalonia.Data;
+using System;
+using System.Reactive.Linq;
+using Avalonia.Collections;
+using Avalonia.Animation;
+using System.Collections.Generic;
+using System.Threading;
+using System.Collections.Concurrent;
namespace Avalonia.Animation
{
@@ -12,45 +19,90 @@ namespace Avalonia.Animation
public class Animatable : AvaloniaObject
{
///
- /// The property transitions for the control.
+ /// Initializes this object.
///
- private PropertyTransitions _propertyTransitions;
+ public Animatable()
+ {
+ Transitions = new Transitions();
+ AnimatableTimer = Timing.AnimationStateTimer
+ .Select(p =>
+ {
+ if (this._playState == PlayState.Pause)
+ {
+ return PlayState.Pause;
+ }
+ else return p;
+ })
+ .Publish()
+ .RefCount();
+ }
///
- /// Gets or sets the property transitions for the control.
+ /// The specific animations timer for this control.
///
- ///
- /// The property transitions for the control.
- ///
- public PropertyTransitions PropertyTransitions
+ ///
+ public IObservable AnimatableTimer;
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty PlayStateProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(PlayState),
+ o => o.PlayState,
+ (o, v) => o.PlayState = v);
+
+ private PlayState _playState = PlayState.Run;
+
+ ///
+ /// Gets or sets the state of the animation for this
+ /// control.
+ ///
+ public PlayState PlayState
{
- get
- {
- return _propertyTransitions ?? (_propertyTransitions = new PropertyTransitions());
- }
+ get { return _playState; }
+ set { SetAndRaise(PlayStateProperty, ref _playState, value); }
- set
- {
- _propertyTransitions = value;
- }
}
+
///
- /// Reacts to a change in a value in order to animate the
- /// change if a is set for the property..
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty> TransitionsProperty =
+ AvaloniaProperty.RegisterDirect>(
+ nameof(Transitions),
+ o => o.Transitions,
+ (o, v) => o.Transitions = v);
+
+ private IEnumerable _transitions = new AvaloniaList();
+
+ ///
+ /// Gets or sets the property transitions for the control.
+ ///
+ public IEnumerable Transitions
+ {
+ get { return _transitions; }
+ set { SetAndRaise(TransitionsProperty, ref _transitions, value); }
+ }
+
+ ///
+ /// Reacts to a change in a value in
+ /// order to animate the change if a is set for the property.
///
/// The event args.
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
- if (e.Priority != BindingPriority.Animation && _propertyTransitions != null)
+ if (e.Priority != BindingPriority.Animation && Transitions != null)
{
- var match = _propertyTransitions.FirstOrDefault(x => x.Property == e.Property);
+ var match = Transitions.FirstOrDefault(x => x.Property == e.Property);
if (match != null)
{
- Animate.Property(this, e.Property, e.OldValue, e.NewValue, match.Easing, match.Duration);
+ match.Apply(this, e.OldValue, e.NewValue);
}
}
}
+
}
}
diff --git a/src/Avalonia.Animation/Animate.cs b/src/Avalonia.Animation/Animate.cs
deleted file mode 100644
index 3e23f635ce..0000000000
--- a/src/Avalonia.Animation/Animate.cs
+++ /dev/null
@@ -1,134 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Diagnostics;
-using System.Linq;
-using System.Reactive.Linq;
-using Avalonia.Data;
-using Avalonia.Threading;
-
-namespace Avalonia.Animation
-{
- ///
- /// Utilities for creating animations.
- ///
- public static class Animate
- {
- ///
- /// The number of frames per second.
- ///
- public const int FramesPerSecond = 60;
-
- ///
- /// The time span of each frame.
- ///
- private static readonly TimeSpan Tick = TimeSpan.FromSeconds(1.0 / FramesPerSecond);
-
- ///
- /// Initializes static members of the class.
- ///
- static Animate()
- {
- Stopwatch = new Stopwatch();
- Stopwatch.Start();
- Timer = Observable.Interval(Tick, AvaloniaScheduler.Instance)
- .Select(_ => Stopwatch.Elapsed)
- .Publish()
- .RefCount();
- }
-
- ///
- /// The stopwatch used to track time.
- ///
- ///
- /// The stopwatch used to track time.
- ///
- public static Stopwatch Stopwatch
- {
- get; }
-
- ///
- /// Gets the animation timer.
- ///
- ///
- /// The animation timer ticks times per second. The
- /// parameter passed to a subsciber is the time span since the animation system was
- /// initialized.
- ///
- ///
- /// The animation timer.
- ///
- public static IObservable Timer
- {
- get; }
-
- ///
- /// Gets a timer that fires every frame for the specified duration.
- ///
- /// The duration of the animation.
- ///
- /// An observable that notifies the subscriber of the progress along the animation.
- ///
- ///
- /// The parameter passed to the subscriber is the progress along the animation, with
- /// 0 being the start and 1 being the end. The observable is guaranteed to fire 0
- /// immediately on subscribe and 1 at the end of the duration.
- ///
- public static IObservable GetTimer(TimeSpan duration)
- {
- var startTime = Stopwatch.Elapsed.Ticks;
- var endTime = startTime + duration.Ticks;
- return Timer
- .TakeWhile(x => x.Ticks < endTime)
- .Select(x => (x.Ticks - startTime) / (double)duration.Ticks)
- .StartWith(0.0)
- .Concat(Observable.Return(1.0));
- }
-
- ///
- /// Animates a .
- ///
- /// The target object.
- /// The target property.
- /// The value of the property at the start of the animation.
- /// The value of the property at the end of the animation.
- /// The easing function to use.
- /// The duration of the animation.
- /// An that can be used to track or stop the animation.
- public static Animation Property(
- IAvaloniaObject target,
- AvaloniaProperty property,
- object start,
- object finish,
- IEasing easing,
- TimeSpan duration)
- {
- var o = GetTimer(duration).Select(progress => easing.Ease(progress, start, finish));
- return new Animation(o, target.Bind(property, o, BindingPriority.Animation));
- }
-
- ///
- /// Animates a .
- ///
- /// The property type.
- /// The target object.
- /// The target property.
- /// The value of the property at the start of the animation.
- /// The value of the property at the end of the animation.
- /// The easing function to use.
- /// The duration of the animation.
- /// An that can be used to track or stop the animation.
- public static Animation Property(
- IAvaloniaObject target,
- AvaloniaProperty property,
- T start,
- T finish,
- IEasing easing,
- TimeSpan duration)
- {
- var o = GetTimer(duration).Select(progress => easing.Ease(progress, start, finish));
- return new Animation(o, target.Bind(property, o, BindingPriority.Animation));
- }
- }
-}
diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs
index 41e4e2fee1..525be53fc2 100644
--- a/src/Avalonia.Animation/Animation.cs
+++ b/src/Avalonia.Animation/Animation.cs
@@ -1,34 +1,118 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Animation.Easings;
+using Avalonia.Animation;
+using Avalonia.Collections;
+using Avalonia.Metadata;
using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Reflection;
+using System.Linq;
namespace Avalonia.Animation
{
///
/// Tracks the progress of an animation.
///
- public class Animation : IObservable
diff --git a/src/Avalonia.Themes.Default/RadioButton.xaml b/src/Avalonia.Themes.Default/RadioButton.xaml
index fb71432595..d29cb89452 100644
--- a/src/Avalonia.Themes.Default/RadioButton.xaml
+++ b/src/Avalonia.Themes.Default/RadioButton.xaml
@@ -8,7 +8,7 @@
diff --git a/src/Avalonia.Themes.Default/TabControl.xaml b/src/Avalonia.Themes.Default/TabControl.xaml
index f8fb4a1f49..0196c1f3ab 100644
--- a/src/Avalonia.Themes.Default/TabControl.xaml
+++ b/src/Avalonia.Themes.Default/TabControl.xaml
@@ -14,7 +14,7 @@
MemberSelector="{x:Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
- Transition="{TemplateBinding Transition}"
+ PageTransition="{TemplateBinding PageTransition}"
Grid.Row="1"/>
diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs
index 8acf6582bb..410ad0a3b3 100644
--- a/src/Avalonia.Visuals/Animation/CrossFade.cs
+++ b/src/Avalonia.Visuals/Animation/CrossFade.cs
@@ -51,6 +51,8 @@ namespace Avalonia.Animation
{
var tasks = new List();
+ // TODO: Implement relevant transition logic here (or discard this class)
+ // in favor of XAML based transition for pages
if (to != null)
{
to.Opacity = 0;
@@ -58,13 +60,6 @@ namespace Avalonia.Animation
if (from != null)
{
- tasks.Add(Animate.Property(
- (IAvaloniaObject)from,
- Visual.OpacityProperty,
- from.Opacity,
- 0,
- LinearEasing.For(),
- Duration).ToTask());
}
if (to != null)
@@ -72,16 +67,10 @@ namespace Avalonia.Animation
to.Opacity = 0;
to.IsVisible = true;
- tasks.Add(Animate.Property(
- (IAvaloniaObject)to,
- Visual.OpacityProperty,
- 0,
- 1,
- LinearEasing.For(),
- Duration).ToTask());
}
- await Task.WhenAll(tasks.ToArray());
+ // FIXME: This is temporary until animations are fixed.
+ await Task.Delay(1);
if (from != null)
{
diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs
index ebc358379d..13f9e67d29 100644
--- a/src/Avalonia.Visuals/Animation/PageSlide.cs
+++ b/src/Avalonia.Visuals/Animation/PageSlide.cs
@@ -74,34 +74,21 @@ namespace Avalonia.Animation
var distance = Orientation == SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height;
var translateProperty = Orientation == SlideAxis.Horizontal ? TranslateTransform.XProperty : TranslateTransform.YProperty;
+
+ // TODO: Implement relevant transition logic here (or discard this class)
+ // in favor of XAML based transition for pages
if (from != null)
{
- var transform = new TranslateTransform();
- from.RenderTransform = transform;
- tasks.Add(Animate.Property(
- transform,
- translateProperty,
- 0.0,
- forward ? -distance : distance,
- LinearEasing.For(),
- Duration).ToTask());
+
}
if (to != null)
{
- var transform = new TranslateTransform();
- to.RenderTransform = transform;
- to.IsVisible = true;
- tasks.Add(Animate.Property(
- transform,
- translateProperty,
- forward ? distance : -distance,
- 0.0,
- LinearEasing.For(),
- Duration).ToTask());
+
}
- await Task.WhenAll(tasks.ToArray());
+ // FIXME: This is temporary until animations are fixed.
+ await Task.Delay(1);
if (from != null)
{
diff --git a/src/Avalonia.Visuals/Animation/PointTransition.cs b/src/Avalonia.Visuals/Animation/PointTransition.cs
new file mode 100644
index 0000000000..4412852781
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/PointTransition.cs
@@ -0,0 +1,31 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Metadata;
+using System;
+using System.Reactive.Linq;
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Transition class that handles with type.
+ ///
+ public class PointTransition : Transition
+ {
+ ///
+ public override IObservable DoTransition(IObservable progress, Point oldValue, Point newValue)
+ {
+ var deltaX = newValue.X - oldValue.Y;
+ var deltaY = newValue.X - oldValue.Y;
+
+ return progress
+ .Select(p =>
+ {
+ var f = Easing.Ease(p);
+ var nX = f * deltaX + oldValue.X;
+ var nY = f * deltaY + oldValue.Y;
+ return new Point(nX, nY);
+ });
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Animation/ThicknessTransition.cs b/src/Avalonia.Visuals/Animation/ThicknessTransition.cs
new file mode 100644
index 0000000000..22876dd8e2
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/ThicknessTransition.cs
@@ -0,0 +1,35 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Metadata;
+using System;
+using System.Reactive.Linq;
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Transition class that handles with type.
+ ///
+ public class ThicknessTransition : Transition
+ {
+ ///
+ public override IObservable DoTransition(IObservable progress, Thickness oldValue, Thickness newValue)
+ {
+ var deltaL = newValue.Left - oldValue.Left;
+ var deltaT = newValue.Top - oldValue.Top;
+ var deltaR = newValue.Right - oldValue.Right;
+ var deltaB = newValue.Bottom - oldValue.Bottom;
+
+ return progress
+ .Select(p =>
+ {
+ var f = Easing.Ease(p);
+ var nL = f * deltaL + oldValue.Left;
+ var nT = f * deltaT + oldValue.Right;
+ var nR = f * deltaR + oldValue.Top;
+ var nB = f * deltaB + oldValue.Bottom;
+ return new Thickness(nL, nT, nR, nB);
+ });
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Animation/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/TransformAnimator.cs
new file mode 100644
index 0000000000..46cefbd061
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/TransformAnimator.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Collections;
+using System.ComponentModel;
+using Avalonia.Animation.Utils;
+using System.Reactive.Linq;
+using System.Linq;
+using Avalonia.Media;
+using Avalonia.Logging;
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Animator that handles properties.
+ ///
+ public class TransformAnimator : Animator
+ {
+ DoubleAnimator childKeyFrames;
+
+ ///
+ public override IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch)
+ {
+ var ctrl = (Visual)control;
+
+ // Check if the Target Property is Transform derived.
+ if (typeof(Transform).IsAssignableFrom(Property.OwnerType))
+ {
+ if (ctrl.RenderTransform == null)
+ {
+ var normalTransform = new TransformGroup();
+
+ // Add the transforms according to MS Expression Blend's
+ // default RenderTransform order.
+
+ normalTransform.Children.Add(new ScaleTransform());
+ normalTransform.Children.Add(new SkewTransform());
+ normalTransform.Children.Add(new RotateTransform());
+ normalTransform.Children.Add(new TranslateTransform());
+
+ ctrl.RenderTransform = normalTransform;
+ }
+
+ var renderTransformType = ctrl.RenderTransform.GetType();
+
+ if (childKeyFrames == null)
+ {
+ InitializeChildKeyFrames();
+ }
+
+ // It's a transform object so let's target that.
+ if (renderTransformType == Property.OwnerType)
+ {
+ return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch);
+ }
+ // It's a TransformGroup and try finding the target there.
+ else if (renderTransformType == typeof(TransformGroup))
+ {
+ foreach (Transform transform in ((TransformGroup)ctrl.RenderTransform).Children)
+ {
+ if (transform.GetType() == Property.OwnerType)
+ {
+ return childKeyFrames.Apply(animation, transform, obsMatch);
+ }
+ }
+ }
+
+ Logger.Warning(
+ LogArea.Animations,
+ control,
+ $"Cannot find the appropriate transform: \"{Property.OwnerType}\" in {control}.");
+ }
+ else
+ {
+ Logger.Error(
+ LogArea.Animations,
+ control,
+ $"Cannot apply animation: Target property owner {Property.OwnerType} is not a Transform object.");
+ }
+ return null;
+ }
+
+ void InitializeChildKeyFrames()
+ {
+ childKeyFrames = new DoubleAnimator();
+
+ foreach (AnimatorKeyFrame keyframe in this)
+ {
+ childKeyFrames.Add(keyframe);
+ }
+
+ childKeyFrames.Property = Property;
+ }
+
+ ///
+ protected override double DoInterpolation(double time, double neutralValue) => 0;
+ }
+}
diff --git a/src/Avalonia.Visuals/Animation/TransformSetter.cs b/src/Avalonia.Visuals/Animation/TransformSetter.cs
new file mode 100644
index 0000000000..1c0319514e
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/TransformSetter.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Diagnostics;
+using Avalonia.Animation.Utils;
+using Avalonia.Data;
+using Avalonia.Media;
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Setter that handles objects
+ /// in the target.
+ ///
+ [Animator(typeof(TransformAnimator))]
+ public class TransformSetter : AnimationSetter
+ {
+
+ }
+}
diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj
index f320644cd0..557cfdbf37 100644
--- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj
+++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj
@@ -1,9 +1,8 @@
netstandard2.0
- Avalonia
-
+
diff --git a/src/Avalonia.Visuals/Media/Brush.cs b/src/Avalonia.Visuals/Media/Brush.cs
index d6b0e43cdc..eef6e1a43c 100644
--- a/src/Avalonia.Visuals/Media/Brush.cs
+++ b/src/Avalonia.Visuals/Media/Brush.cs
@@ -2,14 +2,14 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Linq;
-using System.Reflection;
+using System.ComponentModel;
namespace Avalonia.Media
{
///
/// Describes how an area is painted.
///
+ [TypeConverter(typeof(BrushConverter))]
public abstract class Brush : AvaloniaObject, IBrush
{
///
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/BrushTypeConverter.cs b/src/Avalonia.Visuals/Media/BrushConverter.cs
similarity index 72%
rename from src/Markup/Avalonia.Markup.Xaml/Converters/BrushTypeConverter.cs
rename to src/Avalonia.Visuals/Media/BrushConverter.cs
index e82d93b629..c79360204a 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/BrushTypeConverter.cs
+++ b/src/Avalonia.Visuals/Media/BrushConverter.cs
@@ -2,16 +2,15 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.ComponentModel;
using System.Globalization;
-using Avalonia.Media;
-
-namespace Avalonia.Markup.Xaml.Converters
+namespace Avalonia.Media
{
- using Portable.Xaml.ComponentModel;
- using System.ComponentModel;
-
- public class BrushTypeConverter : TypeConverter
+ ///
+ /// Creates an from a string representation.
+ ///
+ public class BrushConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs
index aacce3fdce..1ee5da1324 100644
--- a/src/Avalonia.Visuals/Media/Color.cs
+++ b/src/Avalonia.Visuals/Media/Color.cs
@@ -140,4 +140,4 @@ namespace Avalonia.Media
return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs
new file mode 100644
index 0000000000..a4b4f5085a
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/FontFamily.cs
@@ -0,0 +1,179 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Media.Fonts;
+
+namespace Avalonia.Media
+{
+ public class FontFamily
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the .
+ /// name
+ public FontFamily(string name)
+ {
+ Contract.Requires(name != null);
+
+ FamilyNames = new FamilyNameCollection(new[] { name });
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The names of the .
+ /// name
+ public FontFamily(IEnumerable names)
+ {
+ Contract.Requires(names != null);
+
+ FamilyNames = new FamilyNameCollection(names);
+ }
+
+ ///
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the .
+ /// The source of font resources.
+ public FontFamily(string name, Uri source) : this(name)
+ {
+ Key = new FontFamilyKey(source);
+ }
+
+ ///
+ /// Represents the default font family
+ ///
+ public static FontFamily Default => new FontFamily("Courier New");
+
+ ///
+ /// Gets the primary family name of the font family.
+ ///
+ ///
+ /// The primary name of the font family.
+ ///
+ public string Name => FamilyNames.PrimaryFamilyName;
+
+ ///
+ /// Gets the family names.
+ ///
+ ///
+ /// The family familyNames.
+ ///
+ public FamilyNameCollection FamilyNames { get; }
+
+ ///
+ /// Gets the key for associated assets.
+ ///
+ ///
+ /// The family familyNames.
+ ///
+ public FontFamilyKey Key { get; }
+
+ ///
+ /// Implicit conversion of string to FontFamily
+ ///
+ ///
+ public static implicit operator FontFamily(string fontFamily)
+ {
+ return new FontFamily(fontFamily);
+ }
+
+ ///
+ /// Parses a string.
+ ///
+ /// The string.
+ ///
+ ///
+ /// Specified family is not supported.
+ ///
+ public static FontFamily Parse(string s)
+ {
+ if (string.IsNullOrEmpty(s))
+ {
+ throw new ArgumentException("Specified family is not supported.");
+ }
+
+ var segments = s.Split('#');
+
+ switch (segments.Length)
+ {
+ case 1:
+ {
+ var names = segments[0].Split(',')
+ .Select(x => x.Trim())
+ .Where(x => !string.IsNullOrWhiteSpace(x));
+ return new FontFamily(names);
+ }
+
+ case 2:
+ {
+ return new FontFamily(segments[1], new Uri(segments[0], UriKind.RelativeOrAbsolute));
+ }
+
+ default:
+ {
+ throw new ArgumentException("Specified family is not supported.");
+ }
+ }
+ }
+
+ ///
+ /// Returns a that represents this instance.
+ ///
+ ///
+ /// A that represents this instance.
+ ///
+ public override string ToString()
+ {
+ if (Key != null)
+ {
+ return Key + "#" + FamilyNames;
+ }
+
+ return FamilyNames.ToString();
+ }
+
+ ///
+ /// Returns a hash code for this instance.
+ ///
+ ///
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ ///
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hash = (int)2186146271;
+
+ hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
+
+ if (Key != null)
+ {
+ hash = (hash * 15768619) ^ Key.GetHashCode();
+ }
+
+ return hash;
+ }
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (!(obj is FontFamily other))
+ {
+ return false;
+ }
+
+ if (Key != null)
+ {
+ return other.FamilyNames.Equals(FamilyNames) && other.Key.Equals(Key);
+ }
+
+ return other.FamilyNames.Equals(FamilyNames);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
new file mode 100644
index 0000000000..50511d2fb7
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
@@ -0,0 +1,131 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace Avalonia.Media.Fonts
+{
+ using System.Text;
+
+ public class FamilyNameCollection : IEnumerable
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The family names.
+ /// familyNames
+ public FamilyNameCollection(IEnumerable familyNames)
+ {
+ Contract.Requires(familyNames != null);
+
+ var names = new List(familyNames);
+
+ if (names.Count == 0) throw new ArgumentException($"{nameof(familyNames)} must not be empty.");
+
+ Names = new ReadOnlyCollection(names);
+
+ PrimaryFamilyName = Names.First();
+
+ HasFallbacks = Names.Count > 1;
+ }
+
+ ///
+ /// Gets the primary family name.
+ ///
+ ///
+ /// The primary family name.
+ ///
+ public string PrimaryFamilyName { get; }
+
+ ///
+ /// Gets a value indicating whether fallbacks are defined.
+ ///
+ ///
+ /// true if fallbacks are defined; otherwise, false.
+ ///
+ public bool HasFallbacks { get; }
+
+ ///
+ /// Gets the internal collection of names.
+ ///
+ ///
+ /// The names.
+ ///
+ internal ReadOnlyCollection Names { get; }
+
+ ///
+ ///
+ /// Returns an enumerator that iterates through the collection.
+ ///
+ ///
+ /// An enumerator that can be used to iterate through the collection.
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ return Names.GetEnumerator();
+ }
+
+ ///
+ ///
+ /// Returns an enumerator that iterates through a collection.
+ ///
+ ///
+ /// An object that can be used to iterate through the collection.
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ ///
+ /// Returns a that represents this instance.
+ ///
+ ///
+ /// A that represents this instance.
+ ///
+ public override string ToString()
+ {
+ var builder = new StringBuilder();
+
+ for (var index = 0; index < Names.Count; index++)
+ {
+ builder.Append(Names[index]);
+
+ if (index == Names.Count - 1) break;
+
+ builder.Append(", ");
+ }
+
+ return builder.ToString();
+ }
+
+ ///
+ /// Returns a hash code for this instance.
+ ///
+ ///
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ ///
+ public override int GetHashCode()
+ {
+ return ToString().GetHashCode();
+ }
+
+ ///
+ /// Determines whether the specified , is equal to this instance.
+ ///
+ /// The to compare with this instance.
+ ///
+ /// true if the specified is equal to this instance; otherwise, false.
+ ///
+ public override bool Equals(object obj)
+ {
+ if (!(obj is FamilyNameCollection other)) return false;
+
+ return other.ToString().Equals(ToString());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs
new file mode 100644
index 0000000000..c2005c9acb
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs
@@ -0,0 +1,106 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Linq;
+
+namespace Avalonia.Media.Fonts
+{
+ ///
+ /// Represents an identifier for a
+ ///
+ public class FontFamilyKey
+ {
+ ///
+ /// Creates a new instance of and extracts and from given
+ ///
+ ///
+ public FontFamilyKey(Uri source)
+ {
+ if (source == null) throw new ArgumentNullException(nameof(source));
+
+ if (source.AbsolutePath.Contains(".ttf"))
+ {
+ var filePathWithoutExtension = source.AbsolutePath.Replace(".ttf", string.Empty);
+ var fileNameWithoutExtension = filePathWithoutExtension.Split('.').Last();
+ FileName = fileNameWithoutExtension + ".ttf";
+ Location = new Uri(source.OriginalString.Replace("." + FileName, string.Empty), UriKind.RelativeOrAbsolute);
+ }
+ else
+ {
+ Location = source;
+ }
+ }
+
+ ///
+ /// Location of stored font asset that belongs to a
+ ///
+ public Uri Location { get; }
+
+ ///
+ /// Optional filename for a font asset that belongs to a
+ ///
+ public string FileName { get; }
+
+ ///
+ /// Returns a hash code for this instance.
+ ///
+ ///
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ ///
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hash = (int)2166136261;
+
+ if (Location != null)
+ {
+ hash = (hash * 16777619) ^ Location.GetHashCode();
+ }
+
+ if (FileName != null)
+ {
+ hash = (hash * 16777619) ^ FileName.GetHashCode();
+ }
+
+ return hash;
+ }
+ }
+
+ ///
+ /// Determines whether the specified , is equal to this instance.
+ ///
+ /// The to compare with this instance.
+ ///
+ /// true if the specified is equal to this instance; otherwise, false.
+ ///
+ public override bool Equals(object obj)
+ {
+ if (!(obj is FontFamilyKey other)) return false;
+
+ if (Location != other.Location) return false;
+
+ if (FileName != other.FileName) return false;
+
+ return true;
+ }
+
+ ///
+ /// Returns a that represents this instance.
+ ///
+ ///
+ /// A that represents this instance.
+ ///
+ public override string ToString()
+ {
+ if (FileName == null) return Location.PathAndQuery;
+
+ var builder = new UriBuilder(Location);
+
+ builder.Path += "." + FileName;
+
+ return builder.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs
new file mode 100644
index 0000000000..2b77371bb5
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs
@@ -0,0 +1,72 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Avalonia.Platform;
+
+namespace Avalonia.Media.Fonts
+{
+ public static class FontFamilyLoader
+ {
+ private static readonly IAssetLoader s_assetLoader;
+
+ static FontFamilyLoader()
+ {
+ s_assetLoader = AvaloniaLocator.Current.GetService();
+ }
+
+ public static IEnumerable LoadFontAssets(FontFamilyKey fontFamilyKey)
+ {
+ return fontFamilyKey.FileName != null
+ ? GetFontAssetsByFileName(fontFamilyKey.Location, fontFamilyKey.FileName)
+ : GetFontAssetsByLocation(fontFamilyKey.Location);
+ }
+
+ ///
+ /// Searches for font assets at a given location and returns a quanity of found assets
+ ///
+ ///
+ ///
+ private static IEnumerable GetFontAssetsByLocation(Uri location)
+ {
+ var availableAssets = s_assetLoader.GetAssets(location);
+
+ var matchingAssets = availableAssets.Where(x => x.absolutePath.EndsWith(".ttf"));
+
+ return matchingAssets.Select(x => GetAssetUri(x.absolutePath, x.assembly));
+ }
+
+ ///
+ /// Searches for font assets at a given location and only accepts assets that fit to a given filename expression.
+ /// File names can target multiple files with * wildcard. For example "FontFile*.ttf"
+ ///
+ ///
+ ///
+ ///
+ private static IEnumerable GetFontAssetsByFileName(Uri location, string fileName)
+ {
+ var availableResources = s_assetLoader.GetAssets(location);
+
+ var compareTo = location.AbsolutePath + "." + fileName.Split('*').First();
+
+ var matchingResources =
+ availableResources.Where(x => x.absolutePath.Contains(compareTo) && x.absolutePath.EndsWith(".ttf"));
+
+ return matchingResources.Select(x => GetAssetUri(x.absolutePath, x.assembly));
+ }
+
+ ///
+ /// Returns a for a font asset that follows the resm scheme
+ ///
+ ///
+ ///
+ ///
+ private static Uri GetAssetUri(string absolutePath, Assembly assembly)
+ {
+ return new Uri("resm:" + absolutePath + "?assembly=" + assembly.GetName().Name, UriKind.RelativeOrAbsolute);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Visuals/Media/Geometry.cs b/src/Avalonia.Visuals/Media/Geometry.cs
index 17c0d2ab74..748d2526af 100644
--- a/src/Avalonia.Visuals/Media/Geometry.cs
+++ b/src/Avalonia.Visuals/Media/Geometry.cs
@@ -69,6 +69,13 @@ namespace Avalonia.Media
set { SetValue(TransformProperty, value); }
}
+ ///
+ /// Creates a from a string.
+ ///
+ /// The string.
+ /// A .
+ public static Geometry Parse(string s) => StreamGeometry.Parse(s);
+
///
/// Clones the geometry.
///
diff --git a/src/Avalonia.Visuals/Media/IBrush.cs b/src/Avalonia.Visuals/Media/IBrush.cs
index 52b4d959cd..bf096076fd 100644
--- a/src/Avalonia.Visuals/Media/IBrush.cs
+++ b/src/Avalonia.Visuals/Media/IBrush.cs
@@ -1,11 +1,14 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using System.ComponentModel;
+
namespace Avalonia.Media
{
///
/// Describes how an area is painted.
///
+ [TypeConverter(typeof(BrushConverter))]
public interface IBrush
{
///
diff --git a/src/Avalonia.Visuals/Media/SolidColorBrush.cs b/src/Avalonia.Visuals/Media/SolidColorBrush.cs
index 6dfee555c9..d090f63069 100644
--- a/src/Avalonia.Visuals/Media/SolidColorBrush.cs
+++ b/src/Avalonia.Visuals/Media/SolidColorBrush.cs
@@ -52,6 +52,21 @@ namespace Avalonia.Media
set { SetValue(ColorProperty, value); }
}
+ ///
+ /// Parses a brush string.
+ ///
+ /// The brush string.
+ /// The .
+ ///
+ /// Whereas may return an immutable solid color brush,
+ /// this method always returns a mutable .
+ ///
+ public static new SolidColorBrush Parse(string s)
+ {
+ var brush = (ISolidColorBrush)Brush.Parse(s);
+ return brush is SolidColorBrush solid ? solid : new SolidColorBrush(brush.Color);
+ }
+
///
/// Returns a string representation of the brush.
///
diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs
index 9c29c62bad..9848a649aa 100644
--- a/src/Avalonia.Visuals/Media/StreamGeometry.cs
+++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs
@@ -33,7 +33,7 @@ namespace Avalonia.Media
///
/// The string.
/// A .
- public static StreamGeometry Parse(string s)
+ public static new StreamGeometry Parse(string s)
{
StreamGeometry result = new StreamGeometry();
diff --git a/src/Avalonia.Visuals/Media/TransformGroup.cs b/src/Avalonia.Visuals/Media/TransformGroup.cs
index 6d94bf974e..dd9a3f9abf 100644
--- a/src/Avalonia.Visuals/Media/TransformGroup.cs
+++ b/src/Avalonia.Visuals/Media/TransformGroup.cs
@@ -3,6 +3,7 @@
using Avalonia.Collections;
using Avalonia.Metadata;
+using System.Collections.Specialized;
namespace Avalonia.Media
{
@@ -17,6 +18,19 @@ namespace Avalonia.Media
public TransformGroup()
{
Children = new Transforms();
+ Children.ResetBehavior = ResetBehavior.Remove;
+ Children.CollectionChanged += delegate
+ {
+ Children.ForEachItem(
+ (tr) => tr.Changed += ChildTransform_Changed,
+ (tr) => tr.Changed -= ChildTransform_Changed,
+ () => { });
+ };
+ }
+
+ private void ChildTransform_Changed(object sender, System.EventArgs e)
+ {
+ this.RaiseChanged();
}
///
diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs
index 12540b67e7..40e98d1565 100644
--- a/src/Avalonia.Visuals/Media/Typeface.cs
+++ b/src/Avalonia.Visuals/Media/Typeface.cs
@@ -10,13 +10,13 @@ namespace Avalonia.Media
///
/// Initializes a new instance of the class.
///
- /// The name of the font family.
+ /// The font family.
/// The font size, in DIPs.
/// The font style.
/// The font weight.
public Typeface(
- string fontFamilyName,
- double fontSize,
+ FontFamily fontFamily,
+ double fontSize = 12,
FontStyle style = FontStyle.Normal,
FontWeight weight = FontWeight.Normal)
{
@@ -30,16 +30,32 @@ namespace Avalonia.Media
throw new ArgumentException("Font weight must be > 0.");
}
- FontFamilyName = fontFamilyName;
+ FontFamily = fontFamily;
FontSize = fontSize;
Style = style;
Weight = weight;
}
///
- /// Gets the name of the font family.
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the font family.
+ /// The font size, in DIPs.
+ /// The font style.
+ /// The font weight.
+ public Typeface(
+ string fontFamilyName,
+ double fontSize = 12,
+ FontStyle style = FontStyle.Normal,
+ FontWeight weight = FontWeight.Normal)
+ : this(new FontFamily(fontFamilyName), fontSize, style, weight)
+ {
+ }
+
+ ///
+ /// Gets the font family.
///
- public string FontFamilyName { get; }
+ public FontFamily FontFamily { get; }
///
/// Gets the size of the font in DIPs.
diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs
index eac362e997..ed464ec7f9 100644
--- a/src/Avalonia.Visuals/Rendering/RendererBase.cs
+++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Rendering
{
_fpsText = new FormattedText
{
- Typeface = new Typeface(null, 18),
+ Typeface = s_fpsTypeface
};
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
index 544892bcfd..9bae6e91c8 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
+++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
@@ -25,10 +25,10 @@
-
-
-
+
+
+
@@ -37,32 +37,15 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs
new file mode 100644
index 0000000000..36c03be65a
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs
@@ -0,0 +1,84 @@
+using System;
+using System.ComponentModel;
+using System.Collections.Generic;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml.Converters;
+using Avalonia.Media.Imaging;
+using Avalonia.Styling;
+using Avalonia.Controls.Templates;
+
+namespace Avalonia.Markup.Xaml
+{
+ ///
+ /// Maintains a repository of s for XAML parsing on top of those
+ /// maintained by .
+ ///
+ ///
+ /// The default method of defining type converters using
+ /// isn't powerful enough for our purposes:
+ ///
+ /// - It doesn't handle non-constructed generic types (such as )
+ /// - Type converters which require XAML features cannot be defined in non-XAML assemblies and
+ /// so can't be referenced using
+ /// - Many types have a static `Parse(string)` method which can be used implicitly; this class
+ /// detects such methods and auto-creates a type converter
+ ///
+ public static class AvaloniaTypeConverters
+ {
+ private static Dictionary _converters = new Dictionary()
+ {
+ { typeof(AvaloniaList<>), typeof(AvaloniaListConverter<>) },
+ { typeof(AvaloniaProperty), typeof(AvaloniaPropertyTypeConverter) },
+ { typeof(IBitmap), typeof(BitmapTypeConverter) },
+ { typeof(IList), typeof(PointsListTypeConverter) },
+ { typeof(IMemberSelector), typeof(MemberSelectorTypeConverter) },
+ { typeof(Selector), typeof(SelectorTypeConverter) },
+ { typeof(TimeSpan), typeof(TimeSpanTypeConverter) },
+ { typeof(WindowIcon), typeof(IconTypeConverter) },
+ };
+
+ ///
+ /// Tries to lookup a for a type.
+ ///
+ /// The type.
+ /// The type converter.
+ public static Type GetTypeConverter(Type type)
+ {
+ if (_converters.TryGetValue(type, out var result))
+ {
+ return result;
+ }
+
+ // Converters for non-constructed generic types can't be specified using
+ // TypeConverterAttribute. Allow them to be registered here and handle them sanely.
+ if (type.IsConstructedGenericType &&
+ _converters.TryGetValue(type.GetGenericTypeDefinition(), out result))
+ {
+ return result?.MakeGenericType(type.GetGenericArguments());
+ }
+
+ // If the type isn't a primitive or a type that XAML already handles, but has a static
+ // Parse method, use that
+ if (!type.IsPrimitive &&
+ type != typeof(DateTime) &&
+ type != typeof(Uri) &&
+ ParseTypeConverter.HasParseMethod(type))
+ {
+ result = typeof(ParseTypeConverter<>).MakeGenericType(type);
+ _converters.Add(type, result);
+ return result;
+ }
+
+ _converters.Add(type, null);
+ return null;
+ }
+
+ ///
+ /// Registers a type converter for a type.
+ ///
+ /// The type. Maybe be a non-constructed generic type.
+ /// The converter type. Maybe be a non-constructed generic type.
+ public static void Register(Type type, Type converterType) => _converters[type] = converterType;
+ }
+}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs
index 79523dd498..f3fac3faf7 100644
--- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs
@@ -126,11 +126,11 @@ namespace Avalonia.Markup.Xaml
}
var asset = assetLocator.OpenAndGetAssembly(uri, baseUri);
- using (var stream = asset.Item1)
+ using (var stream = asset.stream)
{
try
{
- return Load(stream, asset.Item2, rootInstance, uri);
+ return Load(stream, asset.assembly, rootInstance, uri);
}
catch (Exception e)
{
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/ColumnDefinitionsTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/ColumnDefinitionsTypeConverter.cs
deleted file mode 100644
index b0db691a8d..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/ColumnDefinitionsTypeConverter.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-using Avalonia.Controls;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class ColumnDefinitionsTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return new ColumnDefinitions((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs
deleted file mode 100644
index d8c1ecd4eb..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System;
-using System.ComponentModel;
-using System.Globalization;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- public class CornerRadiusTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return CornerRadius.Parse((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/CursorTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/CursorTypeConverter.cs
deleted file mode 100644
index 9639cfc1bd..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/CursorTypeConverter.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-using Avalonia.Input;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class CursorTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- var cursor = (StandardCursorType)Enum.Parse(typeof(StandardCursorType), ((string)value).Trim(), true);
- return new Cursor(cursor);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/GeometryTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/GeometryTypeConverter.cs
deleted file mode 100644
index d7ebc084ae..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/GeometryTypeConverter.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-using Avalonia.Media;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class GeometryTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return StreamGeometry.Parse((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/GridLengthTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/GridLengthTypeConverter.cs
deleted file mode 100644
index 05f3bed04d..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/GridLengthTypeConverter.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-using Avalonia.Controls;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class GridLengthTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return GridLength.Parse((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/KeyGestureConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/KeyGestureConverter.cs
deleted file mode 100644
index 5b11ab0389..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/KeyGestureConverter.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-using Avalonia.Input;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class KeyGestureConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return KeyGesture.Parse((string)value);
- }
- }
-}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs
deleted file mode 100644
index fec6e31771..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class MatrixTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return Matrix.Parse((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs
index fb369610d1..8dc052fe63 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs
@@ -18,10 +18,7 @@ namespace Avalonia.Markup.Xaml.Converters
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
- return new MemberSelector
- {
- MemberName = (string)value,
- };
+ return MemberSelector.Parse((string)value);
}
}
}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/ParseTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/ParseTypeConverter.cs
new file mode 100644
index 0000000000..bfb446fa15
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml/Converters/ParseTypeConverter.cs
@@ -0,0 +1,79 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Reflection;
+
+namespace Avalonia.Markup.Xaml.Converters
+{
+ ///
+ /// Base class for type converters which call a static Parse method.
+ ///
+ public abstract class ParseTypeConverter : TypeConverter
+ {
+ protected const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
+ protected static readonly Type[] StringParameter = new[] { typeof(string) };
+ protected static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) };
+
+ ///
+ /// Checks whether a type has a suitable Parse method.
+ ///
+ /// The type.
+ /// True if the type has a suitable parse method, otherwise false.
+ public static bool HasParseMethod(Type type)
+ {
+ return type.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null) != null ||
+ type.GetMethod("Parse", PublicStatic, null, StringParameter, null) != null;
+ }
+ }
+
+ ///
+ /// A type converter which calls a static Parse method.
+ ///
+ /// The type with the Parse method.
+ public class ParseTypeConverter : ParseTypeConverter
+ {
+ private static Func _parse;
+ private static Func _parseWithFormat;
+
+ static ParseTypeConverter()
+ {
+ var method = typeof(T).GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
+
+ if (method != null)
+ {
+ _parseWithFormat = (Func)method
+ .CreateDelegate(typeof(Func));
+ return;
+ }
+
+ method = typeof(T).GetMethod("Parse", PublicStatic, null, StringParameter, null);
+
+ if (method != null)
+ {
+ _parse = (Func)method.CreateDelegate(typeof(Func));
+ }
+ }
+
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ return sourceType == typeof(string);
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ if (value != null)
+ {
+ if (_parse != null)
+ {
+ return _parse(value.ToString());
+ }
+ else if (_parseWithFormat != null)
+ {
+ return _parseWithFormat(value.ToString(), culture);
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/PointTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/PointTypeConverter.cs
deleted file mode 100644
index 9ca7212c68..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/PointTypeConverter.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class PointTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return Point.Parse((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs
deleted file mode 100644
index 0946f95938..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class RectTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return Rect.Parse((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/RelativePointTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/RelativePointTypeConverter.cs
deleted file mode 100644
index b3178f1496..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/RelativePointTypeConverter.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class RelativePointTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return RelativePoint.Parse((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/RelativeRectTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/RelativeRectTypeConverter.cs
deleted file mode 100644
index 38c2833815..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/RelativeRectTypeConverter.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class RelativeRectTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return RelativeRect.Parse((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/RowDefinitionsTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/RowDefinitionsTypeConverter.cs
deleted file mode 100644
index 64f3a7a04d..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/RowDefinitionsTypeConverter.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-using Avalonia.Controls;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class RowDefinitionsTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return new RowDefinitions((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/SizeTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/SizeTypeConverter.cs
deleted file mode 100644
index 535e7948d5..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/SizeTypeConverter.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class SizeTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return Size.Parse((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/ThicknessTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/ThicknessTypeConverter.cs
deleted file mode 100644
index 3a7652a153..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/ThicknessTypeConverter.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-
-namespace Avalonia.Markup.Xaml.Converters
-{
- using System.ComponentModel;
-
- public class ThicknessTypeConverter : TypeConverter
- {
- public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
- {
- return sourceType == typeof(string);
- }
-
- public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
- {
- return Thickness.Parse((string)value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs
deleted file mode 100644
index 1ae24c8a34..0000000000
--- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Reflection;
-using Avalonia.Markup.Xaml.Converters;
-using Avalonia.Media;
-using Avalonia.Media.Imaging;
-using Avalonia.Styling;
-using Portable.Xaml.ComponentModel;
-using System.ComponentModel;
-using Avalonia.Controls;
-using Avalonia.Input;
-using Avalonia.Collections;
-using Avalonia.Controls.Templates;
-
-namespace Avalonia.Markup.Xaml.PortableXaml
-{
- public class AvaloniaDefaultTypeConverters
- {
- private static Dictionary _defaultConverters = new Dictionary()
- {
- //avalonia default converters
- { typeof(IBitmap), typeof(BitmapTypeConverter)},
- { typeof(IBrush), typeof(BrushTypeConverter) },
- { typeof(Color), typeof(ColorTypeConverter) },
- { typeof(Classes), typeof(ClassesTypeConverter) },
- { typeof(ColumnDefinitions), typeof(ColumnDefinitionsTypeConverter) },
- //{ typeof(DateTime), typeof(DateTimeTypeConverter) },
- { typeof(Geometry), typeof(GeometryTypeConverter) },
- { typeof(GridLength), typeof(GridLengthTypeConverter) },
- { typeof(KeyGesture), typeof(KeyGestureConverter) },
- { typeof(AvaloniaList), typeof(AvaloniaListTypeConverter) },
- { typeof(IMemberSelector), typeof(MemberSelectorTypeConverter) },
- { typeof(Point), typeof(PointTypeConverter) },
- { typeof(Matrix), typeof(MatrixTypeConverter) },
- { typeof(IList), typeof(PointsListTypeConverter) },
- { typeof(AvaloniaProperty), typeof(AvaloniaPropertyTypeConverter) },
- { typeof(RelativePoint), typeof(RelativePointTypeConverter) },
- { typeof(RelativeRect), typeof(RelativeRectTypeConverter) },
- { typeof(RowDefinitions), typeof(RowDefinitionsTypeConverter) },
- { typeof(Size), typeof(SizeTypeConverter) },
- { typeof(Rect), typeof(RectTypeConverter) },
- { typeof(Selector), typeof(SelectorTypeConverter)},
- { typeof(SolidColorBrush), typeof(BrushTypeConverter) },
- { typeof(Thickness), typeof(ThicknessTypeConverter) },
- { typeof(CornerRadius), typeof(CornerRadiusTypeConverter) },
- { typeof(TimeSpan), typeof(TimeSpanTypeConverter) },
- //{ typeof(Uri), typeof(Converters.UriTypeConverter) },
- { typeof(Cursor), typeof(CursorTypeConverter) },
- { typeof(WindowIcon), typeof(IconTypeConverter) },
- //{ typeof(FontWeight), typeof(FontWeightConverter) },
- };
-
- public static Type GetTypeConverter(Type type)
- {
- Type converterType;
-
- if (_defaultConverters.TryGetValue(type, out converterType))
- {
- return converterType;
- }
-
- //TODO: probably a smarter way to handle types
- //is it needed ??
- //Type curType = type;
-
- //while (curType != null)
- //{
- // if (_defaultConverters.TryGetValue(curType, out converterType))
- // {
- // if (curType != type)
- // {
- // _defaultConverters[curType] = converterType;
- // }
- // return converterType;
- // }
- // else
- // {
- // _defaultConverters[curType] = null;
- // }
- // curType = curType.GetTypeInfo().BaseType;
- //}
-
-
-
- return null;
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaTypeAttributeProvider.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaTypeAttributeProvider.cs
index 6e23459cdf..83f4919d4b 100644
--- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaTypeAttributeProvider.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaTypeAttributeProvider.cs
@@ -46,7 +46,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml
if (result == null)
{
- var convType = AvaloniaDefaultTypeConverters.GetTypeConverter(_type);
+ var convType = AvaloniaTypeConverters.GetTypeConverter(_type);
if (convType != null)
{
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
index 5e412633d2..1581765132 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
@@ -25,6 +25,11 @@ namespace Avalonia.Markup.Xaml.Templates
}
}
+ public static MemberSelector Parse(string s)
+ {
+ return new MemberSelector { MemberName = s };
+ }
+
public object Select(object o)
{
if (string.IsNullOrEmpty(MemberName))
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
index f22722a0b5..00f0a48a7b 100644
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
@@ -25,10 +25,34 @@ namespace Avalonia.Skia
// Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B);
- var skiaTypeface = TypefaceCache.GetTypeface(
- typeface?.FontFamilyName ?? "monospace",
- typeface?.Style ?? FontStyle.Normal,
- typeface?.Weight ?? FontWeight.Normal);
+ SKTypeface skiaTypeface = TypefaceCache.Default;
+
+ if (typeface.FontFamily.Key != null)
+ {
+ var typefaces = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
+ skiaTypeface = typefaces.GetTypeFace(typeface);
+ }
+ else
+ {
+ if (typeface.FontFamily.FamilyNames.HasFallbacks)
+ {
+ foreach (var familyName in typeface.FontFamily.FamilyNames)
+ {
+ skiaTypeface = TypefaceCache.GetTypeface(
+ familyName,
+ typeface.Style,
+ typeface.Weight);
+ if (skiaTypeface != TypefaceCache.Default) break;
+ }
+ }
+ else
+ {
+ skiaTypeface = TypefaceCache.GetTypeface(
+ typeface.FontFamily.Name,
+ typeface.Style,
+ typeface.Weight);
+ }
+ }
_paint = new SKPaint();
@@ -36,11 +60,11 @@ namespace Avalonia.Skia
//Paint.TextEncoding = SKTextEncoding.Utf8;
_paint.TextEncoding = SKTextEncoding.Utf16;
_paint.IsStroke = false;
- _paint.IsAntialias = true;
- _paint.LcdRenderText = true;
+ _paint.IsAntialias = true;
+ _paint.LcdRenderText = true;
_paint.SubpixelText = true;
_paint.Typeface = skiaTypeface;
- _paint.TextSize = (float)(typeface?.FontSize ?? 12);
+ _paint.TextSize = (float)typeface.FontSize;
_paint.TextAlign = textAlignment.ToSKTextAlign();
_wrapping = wrapping;
@@ -246,7 +270,7 @@ namespace Avalonia.Skia
subStr = Text.Substring(i, len);
ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering);
-
+
canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint);
i += len;
diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
new file mode 100644
index 0000000000..bd951ef3a3
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
@@ -0,0 +1,74 @@
+using System.Collections.Concurrent;
+using Avalonia.Media;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+ internal class SKTypefaceCollection
+ {
+ private readonly ConcurrentDictionary _cachedTypefaces =
+ new ConcurrentDictionary();
+
+ public void AddTypeFace(SKTypeface typeface)
+ {
+ var key = new FontKey(typeface.FamilyName, (SKFontStyleWeight)typeface.FontWeight, typeface.FontSlant);
+
+ _cachedTypefaces.TryAdd(key, typeface);
+ }
+
+ public SKTypeface GetTypeFace(Typeface typeface)
+ {
+ SKFontStyleSlant skStyle = SKFontStyleSlant.Upright;
+
+ switch (typeface.Style)
+ {
+ case FontStyle.Italic:
+ skStyle = SKFontStyleSlant.Italic;
+ break;
+
+ case FontStyle.Oblique:
+ skStyle = SKFontStyleSlant.Oblique;
+ break;
+ }
+
+ var key = new FontKey(typeface.FontFamily.Name, (SKFontStyleWeight)typeface.Weight, skStyle);
+
+ return _cachedTypefaces.TryGetValue(key, out var skTypeface) ? skTypeface : TypefaceCache.Default;
+ }
+
+ private struct FontKey
+ {
+ public readonly string Name;
+ public readonly SKFontStyleSlant Slant;
+ public readonly SKFontStyleWeight Weight;
+
+ public FontKey(string name, SKFontStyleWeight weight, SKFontStyleSlant slant)
+ {
+ Name = name;
+ Slant = slant;
+ Weight = weight;
+ }
+
+ public override int GetHashCode()
+ {
+ int hash = 17;
+ hash = hash * 31 + Name.GetHashCode();
+ hash = hash * 31 + (int)Slant;
+ hash = hash * 31 + (int)Weight;
+
+ return hash;
+ }
+
+ public override bool Equals(object other)
+ {
+ return other is FontKey ? Equals((FontKey)other) : false;
+ }
+
+ private bool Equals(FontKey other)
+ {
+ return Name == other.Name && Slant == other.Slant &&
+ Weight == other.Weight;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
new file mode 100644
index 0000000000..dc533cbf3c
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
@@ -0,0 +1,53 @@
+using System.Collections.Concurrent;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+ internal static class SKTypefaceCollectionCache
+ {
+ private static readonly ConcurrentDictionary s_cachedCollections;
+
+ static SKTypefaceCollectionCache()
+ {
+ s_cachedCollections = new ConcurrentDictionary();
+ }
+
+ ///
+ /// Gets the or add typeface collection.
+ ///
+ /// The font family.
+ ///
+ public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily)
+ {
+ return s_cachedCollections.GetOrAdd(fontFamily.Key, x => CreateCustomFontCollection(fontFamily));
+ }
+
+ ///
+ /// Creates the custom font collection.
+ ///
+ /// The font family.
+ ///
+ private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily)
+ {
+ var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key);
+
+ var typeFaceCollection = new SKTypefaceCollection();
+
+ var assetLoader = AvaloniaLocator.Current.GetService();
+
+ foreach (var asset in fontAssets)
+ {
+ var assetStream = assetLoader.Open(asset);
+
+ var typeface = SKTypeface.FromStream(assetStream);
+
+ typeFaceCollection.AddTypeFace(typeface);
+ }
+
+ return typeFaceCollection;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs
index c5f3d371ad..24674f3b22 100644
--- a/src/Skia/Avalonia.Skia/TypefaceCache.cs
+++ b/src/Skia/Avalonia.Skia/TypefaceCache.cs
@@ -1,6 +1,5 @@
-using System;
using System.Collections.Generic;
-using System.Text;
+using System.Linq;
using Avalonia.Media;
using SkiaSharp;
@@ -8,6 +7,7 @@ namespace Avalonia.Skia
{
static class TypefaceCache
{
+ public static SKTypeface Default = SKTypeface.FromFamilyName(FontFamily.Default.Name);
static readonly Dictionary> Cache = new Dictionary>();
struct FontKey
@@ -15,9 +15,9 @@ namespace Avalonia.Skia
public readonly SKFontStyleSlant Slant;
public readonly SKFontStyleWeight Weight;
- public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant)
+ public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant)
{
- Slant = slant;
+ Slant = slant;
Weight = weight;
}
@@ -38,35 +38,28 @@ namespace Avalonia.Skia
public bool Equals(FontKey other)
{
return Slant == other.Slant &&
- Weight == other.Weight;
+ Weight == other.Weight;
}
// Equals and GetHashCode ommitted
}
- unsafe static SKTypeface GetTypeface(string name, FontKey key)
+ private static SKTypeface GetTypeface(string name, FontKey key)
{
- if (name == null)
- {
- name = "Arial";
- }
+ var familyKey = name;
- Dictionary entry;
-
- if (!Cache.TryGetValue(name, out entry))
+ if (!Cache.TryGetValue(familyKey, out var entry))
{
- Cache[name] = entry = new Dictionary();
+ Cache[familyKey] = entry = new Dictionary();
}
- SKTypeface typeface = null;
-
- if (!entry.TryGetValue(key, out typeface))
+ if (!entry.TryGetValue(key, out var typeface))
{
- typeface = SKTypeface.FromFamilyName(name, key.Weight, SKFontStyleWidth.Normal, key.Slant);
+ typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant);
- if (typeface == null)
+ if (typeface.FamilyName != name)
{
- typeface = SKTypeface.FromFamilyName(null, SKTypefaceStyle.Normal);
+ typeface = Default;
}
entry[key] = typeface;
@@ -79,7 +72,7 @@ namespace Avalonia.Skia
{
SKFontStyleSlant skStyle = SKFontStyleSlant.Upright;
- switch(style)
+ switch (style)
{
case FontStyle.Italic:
skStyle = SKFontStyleSlant.Italic;
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontFileEnumerator.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontFileEnumerator.cs
new file mode 100644
index 0000000000..c144e12aea
--- /dev/null
+++ b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontFileEnumerator.cs
@@ -0,0 +1,67 @@
+using SharpDX;
+using SharpDX.DirectWrite;
+
+namespace Avalonia.Direct2D1.Media
+{
+ ///
+ /// Resource FontFileEnumerator.
+ ///
+ public class DWriteResourceFontFileEnumerator : CallbackBase, FontFileEnumerator
+ {
+ private readonly Factory _factory;
+ private readonly FontFileLoader _loader;
+ private readonly DataStream _keyStream;
+ private FontFile _currentFontFile;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The factory.
+ /// The loader.
+ /// The key.
+ public DWriteResourceFontFileEnumerator(Factory factory, FontFileLoader loader, DataPointer key)
+ {
+ _factory = factory;
+ _loader = loader;
+ _keyStream = new DataStream(key.Pointer, key.Size, true, false);
+ }
+
+ ///
+ /// Advances to the next font file in the collection. When it is first created, the enumerator is positioned before the first element of the collection and the first call to MoveNext advances to the first file.
+ ///
+ ///
+ /// the value TRUE if the enumerator advances to a file; otherwise, FALSE if the enumerator advances past the last file in the collection.
+ ///
+ /// HRESULT IDWriteFontFileEnumerator::MoveNext([Out] BOOL* hasCurrentFile)
+ bool FontFileEnumerator.MoveNext()
+ {
+ bool moveNext = _keyStream.RemainingLength != 0;
+
+ if (!moveNext) return false;
+
+ _currentFontFile?.Dispose();
+
+ _currentFontFile = new FontFile(_factory, _keyStream.PositionPointer, 4, _loader);
+
+ _keyStream.Position += 4;
+
+ return true;
+ }
+
+ ///
+ /// Gets a reference to the current font file.
+ ///
+ ///
+ /// a reference to the newly created object.
+ /// HRESULT IDWriteFontFileEnumerator::GetCurrentFontFile([Out] IDWriteFontFile** fontFile)
+ FontFile FontFileEnumerator.CurrentFontFile
+ {
+ get
+ {
+ ((IUnknown)_currentFontFile).AddReference();
+
+ return _currentFontFile;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontFileStream.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontFileStream.cs
new file mode 100644
index 0000000000..1b1cb85495
--- /dev/null
+++ b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontFileStream.cs
@@ -0,0 +1,84 @@
+using System;
+using SharpDX;
+using SharpDX.DirectWrite;
+
+namespace Avalonia.Direct2D1.Media
+{
+ ///
+ /// This FontFileStream implem is reading data from a .
+ ///
+ public class DWriteResourceFontFileStream : CallbackBase, FontFileStream
+ {
+ private readonly DataStream _stream;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The stream.
+ public DWriteResourceFontFileStream(DataStream stream)
+ {
+ _stream = stream;
+ }
+
+ ///
+ /// Reads a fragment from a font file.
+ ///
+ /// When this method returns, contains an address of a reference to the start of the font file fragment. This parameter is passed uninitialized.
+ /// The offset of the fragment, in bytes, from the beginning of the font file.
+ /// The size of the file fragment, in bytes.
+ /// When this method returns, contains the address of
+ ///
+ /// Note that ReadFileFragment implementations must check whether the requested font file fragment is within the file bounds. Otherwise, an error should be returned from ReadFileFragment. {{DirectWrite}} may invoke methods on the same object from multiple threads simultaneously. Therefore, ReadFileFragment implementations that rely on internal mutable state must serialize access to such state across multiple threads. For example, an implementation that uses separate Seek and Read operations to read a file fragment must place the code block containing Seek and Read calls under a lock or a critical section.
+ ///
+ /// HRESULT IDWriteFontFileStream::ReadFileFragment([Out, Buffer] const void** fragmentStart,[None] __int64 fileOffset,[None] __int64 fragmentSize,[Out] void** fragmentContext)
+ void FontFileStream.ReadFileFragment(out IntPtr fragmentStart, long fileOffset, long fragmentSize, out IntPtr fragmentContext)
+ {
+ lock (this)
+ {
+ fragmentContext = IntPtr.Zero;
+
+ _stream.Position = fileOffset;
+
+ fragmentStart = _stream.PositionPointer;
+ }
+ }
+
+ ///
+ /// Releases a fragment from a file.
+ ///
+ /// A reference to the client-defined context of a font fragment returned from {{ReadFileFragment}}.
+ /// void IDWriteFontFileStream::ReleaseFileFragment([None] void* fragmentContext)
+ void FontFileStream.ReleaseFileFragment(IntPtr fragmentContext)
+ {
+ // Nothing to release. No context are used
+ }
+
+ ///
+ /// Obtains the total size of a file.
+ ///
+ /// the total size of the file.
+ ///
+ /// Implementing GetFileSize() for asynchronously loaded font files may require downloading the complete file contents. Therefore, this method should be used only for operations that either require a complete font file to be loaded (for example, copying a font file) or that need to make decisions based on the value of the file size (for example, validation against a persisted file size).
+ ///
+ /// HRESULT IDWriteFontFileStream::GetFileSize([Out] __int64* fileSize)
+ long FontFileStream.GetFileSize()
+ {
+ return _stream.Length;
+ }
+
+ ///
+ /// Obtains the last modified time of the file.
+ ///
+ ///
+ /// the last modified time of the file in the format that represents the number of 100-nanosecond intervals since January 1, 1601 (UTC).
+ ///
+ ///
+ /// The "last modified time" is used by DirectWrite font selection algorithms to determine whether one font resource is more up to date than another one.
+ ///
+ /// HRESULT IDWriteFontFileStream::GetLastWriteTime([Out] __int64* lastWriteTime)
+ long FontFileStream.GetLastWriteTime()
+ {
+ return 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs
new file mode 100644
index 0000000000..065da8d3e8
--- /dev/null
+++ b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using Avalonia.Media.Fonts;
+using Avalonia.Platform;
+using SharpDX;
+using SharpDX.DirectWrite;
+
+namespace Avalonia.Direct2D1.Media
+{
+ using System;
+
+ internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader
+ {
+ private readonly List _fontStreams = new List();
+ private readonly List _enumerators = new List();
+ private readonly DataStream _keyStream;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The factory.
+ ///
+ public DWriteResourceFontLoader(Factory factory, IEnumerable fontAssets)
+ {
+ var factory1 = factory;
+
+ var assetLoader = AvaloniaLocator.Current.GetService();
+
+ foreach (var asset in fontAssets)
+ {
+ var assetStream = assetLoader.Open(asset);
+
+ var dataStream = new DataStream((int)assetStream.Length, true, true);
+
+ assetStream.CopyTo(dataStream);
+
+ dataStream.Position = 0;
+
+ _fontStreams.Add(new DWriteResourceFontFileStream(dataStream));
+ }
+
+ // Build a Key storage that stores the index of the font
+ _keyStream = new DataStream(sizeof(int) * _fontStreams.Count, true, true);
+
+ for (int i = 0; i < _fontStreams.Count; i++)
+ {
+ _keyStream.Write(i);
+ }
+
+ _keyStream.Position = 0;
+
+ // Register the
+ factory1.RegisterFontFileLoader(this);
+ factory1.RegisterFontCollectionLoader(this);
+ }
+
+
+ ///
+ /// Gets the key used to identify the FontCollection as well as storing index for fonts.
+ ///
+ /// The key.
+ public DataStream Key => _keyStream;
+
+ ///
+ /// Creates a font file enumerator object that encapsulates a collection of font files. The font system calls back to this interface to create a font collection.
+ ///
+ /// Pointer to the object that was used to create the current font collection.
+ /// A font collection key that uniquely identifies the collection of font files within the scope of the font collection loader being used. The buffer allocated for this key must be at least the size, in bytes, specified by collectionKeySize.
+ ///
+ /// a reference to the newly created font file enumerator.
+ ///
+ /// HRESULT IDWriteFontCollectionLoader::CreateEnumeratorFromKey([None] IDWriteFactory* factory,[In, Buffer] const void* collectionKey,[None] int collectionKeySize,[Out] IDWriteFontFileEnumerator** fontFileEnumerator)
+ FontFileEnumerator FontCollectionLoader.CreateEnumeratorFromKey(Factory factory, DataPointer collectionKey)
+ {
+ var enumerator = new DWriteResourceFontFileEnumerator(factory, this, collectionKey);
+
+ _enumerators.Add(enumerator);
+
+ return enumerator;
+ }
+
+ ///
+ /// Creates a font file stream object that encapsulates an open file resource.
+ ///
+ /// A reference to a font file reference key that uniquely identifies the font file resource within the scope of the font loader being used. The buffer allocated for this key must at least be the size, in bytes, specified by fontFileReferenceKeySize.
+ ///
+ /// a reference to the newly created object.
+ ///
+ ///
+ /// The resource is closed when the last reference to fontFileStream is released.
+ ///
+ /// HRESULT IDWriteFontFileLoader::CreateStreamFromKey([In, Buffer] const void* fontFileReferenceKey,[None] int fontFileReferenceKeySize,[Out] IDWriteFontFileStream** fontFileStream)
+ FontFileStream FontFileLoader.CreateStreamFromKey(DataPointer fontFileReferenceKey)
+ {
+ var index = SharpDX.Utilities.Read(fontFileReferenceKey.Pointer);
+
+ return _fontStreams[index];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
new file mode 100644
index 0000000000..4199c73c54
--- /dev/null
+++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
@@ -0,0 +1,65 @@
+using System.Collections.Concurrent;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+
+namespace Avalonia.Direct2D1.Media
+{
+ internal static class Direct2D1FontCollectionCache
+ {
+ private static readonly ConcurrentDictionary s_cachedCollections;
+ private static readonly SharpDX.DirectWrite.Factory s_factory;
+ private static readonly SharpDX.DirectWrite.FontCollection s_installedFontCollection;
+
+ static Direct2D1FontCollectionCache()
+ {
+ s_cachedCollections = new ConcurrentDictionary();
+
+ s_factory = AvaloniaLocator.Current.GetService();
+
+ s_installedFontCollection = s_factory.GetSystemFontCollection(false);
+ }
+
+ public static SharpDX.DirectWrite.TextFormat GetTextFormat(Typeface typeface)
+ {
+ var fontFamily = typeface.FontFamily;
+ var fontCollection = GetOrAddFontCollection(fontFamily);
+ var fontFamilyName = FontFamily.Default.Name;
+
+ // Should this be cached?
+ foreach (var familyName in fontFamily.FamilyNames)
+ {
+ if (!fontCollection.FindFamilyName(familyName, out _))
+ {
+ continue;
+ }
+
+ fontFamilyName = familyName;
+
+ break;
+ }
+
+ return new SharpDX.DirectWrite.TextFormat(
+ s_factory,
+ fontFamilyName,
+ fontCollection,
+ (SharpDX.DirectWrite.FontWeight)typeface.Weight,
+ (SharpDX.DirectWrite.FontStyle)typeface.Style,
+ SharpDX.DirectWrite.FontStretch.Normal,
+ (float)typeface.FontSize);
+ }
+
+ private static SharpDX.DirectWrite.FontCollection GetOrAddFontCollection(FontFamily fontFamily)
+ {
+ return fontFamily.Key == null ? s_installedFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection);
+ }
+
+ private static SharpDX.DirectWrite.FontCollection CreateFontCollection(FontFamilyKey key)
+ {
+ var assets = FontFamilyLoader.LoadFontAssets(key);
+
+ var fontLoader = new DWriteResourceFontLoader(s_factory, assets);
+
+ return new SharpDX.DirectWrite.FontCollection(s_factory, fontLoader, fontLoader.Key);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
index 5578abc32c..0f09a1827b 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
@@ -1,7 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
-using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media;
@@ -21,29 +20,25 @@ namespace Avalonia.Direct2D1.Media
IReadOnlyList spans)
{
Text = text;
+
var factory = AvaloniaLocator.Current.GetService();
- using (var format = new DWrite.TextFormat(
- factory,
- typeface?.FontFamilyName ?? "Courier New",
- (DWrite.FontWeight)(typeface?.Weight ?? FontWeight.Normal),
- (DWrite.FontStyle)(typeface?.Style ?? FontStyle.Normal),
- (float)(typeface?.FontSize ?? 12)))
+ var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface);
+
+ textFormat.WordWrapping =
+ wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap;
+
+ TextLayout = new DWrite.TextLayout(
+ factory,
+ Text ?? string.Empty,
+ textFormat,
+ (float)constraint.Width,
+ (float)constraint.Height)
{
- format.WordWrapping = wrapping == TextWrapping.Wrap ?
- DWrite.WordWrapping.Wrap :
- DWrite.WordWrapping.NoWrap;
-
- TextLayout = new DWrite.TextLayout(
- factory,
- text ?? string.Empty,
- format,
- (float)constraint.Width,
- (float)constraint.Height)
- {
- TextAlignment = textAlignment.ToDirect2D()
- };
- }
+ TextAlignment = textAlignment.ToDirect2D()
+ };
+
+ textFormat.Dispose();
if (spans != null)
{
@@ -64,11 +59,6 @@ namespace Avalonia.Direct2D1.Media
public DWrite.TextLayout TextLayout { get; }
- public void Dispose()
- {
- TextLayout.Dispose();
- }
-
public IEnumerable GetLines()
{
var result = TextLayout.GetLineMetrics();
@@ -77,14 +67,11 @@ namespace Avalonia.Direct2D1.Media
public TextHitTestResult HitTestPoint(Point point)
{
- SharpDX.Mathematics.Interop.RawBool isTrailingHit;
- SharpDX.Mathematics.Interop.RawBool isInside;
-
var result = TextLayout.HitTestPoint(
(float)point.X,
(float)point.Y,
- out isTrailingHit,
- out isInside);
+ out var isTrailingHit,
+ out var isInside);
return new TextHitTestResult
{
@@ -96,14 +83,7 @@ namespace Avalonia.Direct2D1.Media
public Rect HitTestTextPosition(int index)
{
- float x;
- float y;
-
- var result = TextLayout.HitTestTextPosition(
- index,
- false,
- out x,
- out y);
+ var result = TextLayout.HitTestTextPosition(index, false, out _, out _);
return new Rect(result.Left, result.Top, result.Width, result.Height);
}
diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
index fd731455d8..8a38a00493 100644
--- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
using Xunit;
diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
index 4ce83056c6..4d38ec9856 100644
--- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
@@ -102,7 +102,7 @@ namespace Avalonia.Controls.UnitTests
[~CarouselPresenter.ItemsProperty] = control[~Carousel.ItemsProperty],
[~CarouselPresenter.ItemsPanelProperty] = control[~Carousel.ItemsPanelProperty],
[~CarouselPresenter.SelectedIndexProperty] = control[~Carousel.SelectedIndexProperty],
- [~CarouselPresenter.TransitionProperty] = control[~Carousel.TransitionProperty],
+ [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty],
};
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs
index 67b224d6a0..322c14c6bd 100644
--- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs
@@ -313,7 +313,7 @@ namespace Avalonia.Controls.UnitTests
[!CarouselPresenter.ItemsPanelProperty] = control[!ItemsControl.ItemsPanelProperty],
[!CarouselPresenter.MemberSelectorProperty] = control[!ItemsControl.MemberSelectorProperty],
[!CarouselPresenter.SelectedIndexProperty] = control[!SelectingItemsControl.SelectedIndexProperty],
- [~CarouselPresenter.TransitionProperty] = control[~Carousel.TransitionProperty],
+ [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty],
};
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
index 4213a31c39..0fcb9432ab 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
@@ -530,7 +530,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
style.TryGetResource("Brush", out var brush);
Assert.NotNull(brush);
-
+ Assert.IsType(brush);
Assert.Equal(Colors.White, ((ISolidColorBrush)brush).Color);
style.TryGetResource("Double", out var d);
diff --git a/tests/Avalonia.RenderTests/Controls/BorderTests.cs b/tests/Avalonia.RenderTests/Controls/BorderTests.cs
index 7d2e40c3b4..c82d616094 100644
--- a/tests/Avalonia.RenderTests/Controls/BorderTests.cs
+++ b/tests/Avalonia.RenderTests/Controls/BorderTests.cs
@@ -204,7 +204,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
{
Text = "Foo",
Background = Brushes.Red,
- FontFamily = "Segoe UI",
+ FontFamily = new FontFamily("Segoe UI"),
FontSize = 12,
HorizontalAlignment = HorizontalAlignment.Center,
}
@@ -231,7 +231,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
{
Text = "Foo",
Background = Brushes.Red,
- FontFamily = "Segoe UI",
+ FontFamily = new FontFamily("Segoe UI"),
FontSize = 12,
VerticalAlignment = VerticalAlignment.Center,
}
@@ -258,7 +258,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
{
Text = "Foo",
Background = Brushes.Red,
- FontFamily = "Segoe UI",
+ FontFamily = new FontFamily("Segoe UI"),
FontSize = 12,
HorizontalAlignment = HorizontalAlignment.Stretch,
}
@@ -285,7 +285,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
{
Text = "Foo",
Background = Brushes.Red,
- FontFamily = "Segoe UI",
+ FontFamily = new FontFamily("Segoe UI"),
FontSize = 12,
VerticalAlignment = VerticalAlignment.Stretch,
}
@@ -312,7 +312,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
{
Text = "Foo",
Background = Brushes.Red,
- FontFamily = "Segoe UI",
+ FontFamily = new FontFamily("Segoe UI"),
FontSize = 12,
HorizontalAlignment = HorizontalAlignment.Left,
}
@@ -339,7 +339,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
{
Text = "Foo",
Background = Brushes.Red,
- FontFamily = "Segoe UI",
+ FontFamily = new FontFamily("Segoe UI"),
FontSize = 12,
HorizontalAlignment = HorizontalAlignment.Right,
}
@@ -366,7 +366,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
{
Text = "Foo",
Background = Brushes.Red,
- FontFamily = "Segoe UI",
+ FontFamily = new FontFamily("Segoe UI"),
FontSize = 12,
VerticalAlignment = VerticalAlignment.Top,
}
@@ -393,7 +393,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
{
Text = "Foo",
Background = Brushes.Red,
- FontFamily = "Segoe UI",
+ FontFamily = new FontFamily("Segoe UI"),
FontSize = 12,
VerticalAlignment = VerticalAlignment.Bottom,
}
diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
index cfa15ae304..f69d336271 100644
--- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
+++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
@@ -48,7 +48,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Child = new TextBlock
{
FontSize = 24,
- FontFamily = "Arial",
+ FontFamily = new FontFamily("Arial"),
Background = Brushes.Green,
Foreground = Brushes.Yellow,
Text = "VisualBrush",
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs
new file mode 100644
index 0000000000..4c7a89cbda
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs
@@ -0,0 +1,91 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+ public class FontFamilyTests
+ {
+ [Fact]
+ public void Exception_Should_Be_Thrown_If_Name_Is_Null()
+ {
+ Assert.Throws(() => new FontFamily((string)null));
+ }
+
+ [Fact]
+ public void Exception_Should_Be_Thrown_If_Names_Is_Null()
+ {
+ Assert.Throws(() => new FontFamily((IEnumerable)null));
+ }
+
+ [Fact]
+ public void Should_Implicitly_Convert_String_To_FontFamily()
+ {
+ FontFamily fontFamily = "Arial";
+
+ Assert.Equal(new FontFamily("Arial"), fontFamily);
+ }
+
+ [Fact]
+ public void Should_Be_Equal()
+ {
+ var fontFamily = new FontFamily("Arial");
+
+ Assert.Equal(new FontFamily("Arial"), fontFamily);
+ }
+
+ [Fact]
+ public void Parse_Parses_FontFamily_With_Name()
+ {
+ var fontFamily = FontFamily.Parse("Courier New");
+
+ Assert.Equal("Courier New", fontFamily.Name);
+ }
+
+ [Fact]
+ public void Parse_Parses_FontFamily_With_Names()
+ {
+ var fontFamily = FontFamily.Parse("Courier New, Times New Roman");
+
+ Assert.Equal("Courier New", fontFamily.Name);
+
+ Assert.Equal(2, fontFamily.FamilyNames.Count());
+
+ Assert.Equal("Times New Roman", fontFamily.FamilyNames.Last());
+ }
+
+ [Fact]
+ public void Parse_Parses_FontFamily_With_Resource_Folder()
+ {
+ var source = new Uri("resm:Avalonia.Visuals.UnitTests#MyFont");
+
+ var key = new FontFamilyKey(source);
+
+ var fontFamily = FontFamily.Parse(source.OriginalString);
+
+ Assert.Equal("MyFont", fontFamily.Name);
+
+ Assert.Equal(key, fontFamily.Key);
+ }
+
+ [Fact]
+ public void Parse_Parses_FontFamily_With_Resource_Filename()
+ {
+ var source = new Uri("resm:Avalonia.Visuals.UnitTests.MyFont.ttf#MyFont");
+
+ var key = new FontFamilyKey(source);
+
+ var fontFamily = FontFamily.Parse(source.OriginalString);
+
+ Assert.Equal("MyFont", fontFamily.Name);
+
+ Assert.Equal(key, fontFamily.Key);
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FamilyNameCollectionTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FamilyNameCollectionTests.cs
new file mode 100644
index 0000000000..14daf5bd8b
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FamilyNameCollectionTests.cs
@@ -0,0 +1,33 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Linq;
+using Avalonia.Media.Fonts;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media.Fonts
+{
+ public class FamilyNameCollectionTests
+ {
+ [Fact]
+ public void Exception_Should_Be_Thrown_If_Names_Is_Null()
+ {
+ Assert.Throws(() => new FamilyNameCollection(null));
+ }
+
+ [Fact]
+ public void Exception_Should_Be_Thrown_If_Names_Is_Empty()
+ {
+ Assert.Throws(() => new FamilyNameCollection(Enumerable.Empty()));
+ }
+
+ [Fact]
+ public void Should_Be_Equal()
+ {
+ var familyNames = new FamilyNameCollection(new[] { "Arial", "Times New Roman" });
+
+ Assert.Equal(new FamilyNameCollection(new[] { "Arial", "Times New Roman" }), familyNames);
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyKeyTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyKeyTests.cs
new file mode 100644
index 0000000000..96464b5784
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyKeyTests.cs
@@ -0,0 +1,42 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Media.Fonts;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media.Fonts
+{
+ public class FontFamilyKeyTests
+ {
+ [Fact]
+ public void Exception_Should_Be_Thrown_If_Source_Is_Null()
+ {
+ Assert.Throws(() => new FontFamilyKey(null));
+ }
+
+ [Fact]
+ public void Should_Initialize_With_Location()
+ {
+ var source = new Uri("resm:Avalonia.Visuals.UnitTests#MyFont");
+
+ var fontFamilyKey = new FontFamilyKey(source);
+
+ Assert.Equal(new Uri("resm:Avalonia.Visuals.UnitTests"), fontFamilyKey.Location);
+
+ Assert.Null(fontFamilyKey.FileName);
+ }
+
+ [Fact]
+ public void Should_Initialize_With_Location_And_Filename()
+ {
+ var source = new Uri("resm:Avalonia.Visuals.UnitTests.MyFont.ttf#MyFont");
+
+ var fontFamilyKey = new FontFamilyKey(source);
+
+ Assert.Equal(new Uri("resm:Avalonia.Visuals.UnitTests"), fontFamilyKey.Location);
+
+ Assert.Equal("MyFont.ttf", fontFamilyKey.FileName);
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs
new file mode 100644
index 0000000000..e65c78e285
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs
@@ -0,0 +1,81 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Linq;
+using Avalonia.Media.Fonts;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media.Fonts
+{
+ using System.Diagnostics;
+
+ public class FontFamilyLoaderTests : IDisposable
+ {
+ private const string FontName = "#MyFont";
+ private const string Assembly = "?assembly=Avalonia.Visuals.UnitTests";
+ private const string AssetLocation = "resm:Avalonia.Visuals.UnitTests.Assets";
+
+ private readonly IDisposable _testApplication;
+
+ public FontFamilyLoaderTests()
+ {
+ const string AssetMyFontRegular = AssetLocation + ".MyFont-Regular.ttf" + Assembly + FontName;
+ const string AssetMyFontBold = AssetLocation + ".MyFont-Bold.ttf" + Assembly + FontName;
+ const string AssetYourFont = AssetLocation + ".YourFont.ttf" + Assembly + FontName;
+
+ var fontAssets = new[]
+ {
+ (AssetMyFontRegular, "AssetData"),
+ (AssetMyFontBold, "AssetData"),
+ (AssetYourFont, "AssetData")
+ };
+
+ _testApplication = StartWithResources(fontAssets);
+ }
+
+ public void Dispose()
+ {
+ _testApplication.Dispose();
+ }
+
+ [Fact]
+ public void Should_Load_Single_FontAsset()
+ {
+ const string FontAsset = AssetLocation + ".MyFont-Regular.ttf" + Assembly + FontName;
+
+ var source = new Uri(FontAsset, UriKind.RelativeOrAbsolute);
+
+ var key = new FontFamilyKey(source);
+
+ var fontAssets = FontFamilyLoader.LoadFontAssets(key);
+
+ Assert.Single(fontAssets);
+ }
+
+ [Fact]
+ public void Should_Load_Matching_Assets()
+ {
+ var source = new Uri(AssetLocation + ".MyFont-*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute);
+
+ var key = new FontFamilyKey(source);
+
+ var fontAssets = FontFamilyLoader.LoadFontAssets(key).ToArray();
+
+ foreach (var fontAsset in fontAssets)
+ {
+ Debug.WriteLine(fontAsset);
+ }
+
+ Assert.Equal(2, fontAssets.Length);
+ }
+
+ private static IDisposable StartWithResources(params (string, string)[] assets)
+ {
+ var assetLoader = new MockAssetLoader(assets);
+ var services = new TestServices(assetLoader: assetLoader, platform: new AppBuilder().RuntimePlatform);
+ return UnitTestApplication.Start(services);
+ }
+ }
+}