diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index acff8cc117..46e8665945 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -18,11 +18,13 @@
- [ ] Added unit tests (if possible)?
- [ ] Added XML documentation to any related classes?
-- [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Avaloniaui.net with user documentation
+- [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Documentation with user documentation
## Breaking changes
+## Obsoletions / Deprecations
+
## Fixed issues
-
diff --git a/readme.md b/readme.md
index f73bdffaeb..7959bc5540 100644
--- a/readme.md
+++ b/readme.md
@@ -2,8 +2,6 @@
[](https://www.nuget.org/packages/Avalonia) [](https://www.nuget.org/packages/Avalonia) [](https://www.myget.org/gallery/avalonia-ci) 
-
-
## 📖 About AvaloniaUI
Avalonia is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows via .NET Framework and .NET Core, Linux via Xorg, macOS. Avalonia is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development.
@@ -16,7 +14,7 @@ To see the status of some of our features, please see our [Roadmap](https://gith
## 🚀 Getting Started
-The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](https://avaloniaui.net/docs/quickstart/create-new-project).
+The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](https://docs.avaloniaui.net/docs/getting-started).
Avalonia is delivered via NuGet package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/
@@ -54,7 +52,7 @@ We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using
## Documentation
-Documentation can be found on our website at https://avaloniaui.net/docs/. We also have a [tutorial](https://avaloniaui.net/docs/tutorial/) over there for newcomers.
+Documentation can be found at https://docs.avaloniaui.net. We also have a [tutorial](https://docs.avaloniaui.net/docs/getting-started/programming-with-avalonia) over there for newcomers.
## Building and Using
diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
index d5aedf7783..3c2d2ee359 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -1,7 +1,7 @@
- Exe
+ WinExe
netcoreapp3.1
true
@@ -15,6 +15,10 @@
+
+
+ en
+
diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs
index 020fb2fff3..f3ec7b48aa 100644
--- a/samples/ControlCatalog/App.xaml.cs
+++ b/samples/ControlCatalog/App.xaml.cs
@@ -39,6 +39,10 @@ namespace ControlCatalog
public static Styles DefaultLight = new Styles
{
+ new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
+ {
+ Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml")
+ },
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")
@@ -60,6 +64,10 @@ namespace ControlCatalog
public static Styles DefaultDark = new Styles
{
+ new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
+ {
+ Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml")
+ },
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index 1aa926a2a6..53ad213d92 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -27,6 +27,6 @@
-
+
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 142c532d75..6537c470d5 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -21,6 +21,9 @@
+
+
+
@@ -38,6 +41,9 @@
+
+
+
diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml
index 6a70bb082f..a107ee2163 100644
--- a/samples/ControlCatalog/MainWindow.xaml
+++ b/samples/ControlCatalog/MainWindow.xaml
@@ -18,11 +18,11 @@
-
+
-
+
diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
new file mode 100644
index 0000000000..e15637aa0f
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+ Context Flyout
+ A right click Flyout that can be applied to any control.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs
new file mode 100644
index 0000000000..e64d4a2cdd
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs
@@ -0,0 +1,45 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+using Avalonia.Interactivity;
+namespace ControlCatalog.Pages
+{
+ public class ContextFlyoutPage : UserControl
+ {
+ private TextBox _textBox;
+
+ public ContextFlyoutPage()
+ {
+ InitializeComponent();
+
+ var vm = new ContextFlyoutPageViewModel();
+ vm.View = this;
+ DataContext = vm;
+
+ _textBox = this.FindControl("TextBox");
+
+ var cutButton = this.FindControl
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml
index 3ff47111eb..79560be933 100644
--- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml
@@ -1,6 +1,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
index 0acb8f4f6e..51836255ef 100644
--- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
@@ -59,4 +59,8 @@
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml b/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml
new file mode 100644
index 0000000000..92f8177ead
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml
@@ -0,0 +1,42 @@
+
+
+ 1
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
new file mode 100644
index 0000000000..7ba5440591
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
@@ -0,0 +1,324 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml
index 34e6f173d5..16b1d5e239 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml
@@ -12,7 +12,6 @@
32
- 12,0,12,0
diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml
new file mode 100644
index 0000000000..ff50acab5e
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml
@@ -0,0 +1,35 @@
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
index 4c098afe16..f199f80fed 100644
--- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
@@ -43,8 +43,7 @@
- -4
- 0,4,0,4
+ -4
0,0,12,0
24,0,0,0
M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z
diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
index 139a61abdf..300a958eb7 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
@@ -14,6 +14,13 @@
M 11.416016,10 20,1.4160156 18.583984,0 10,8.5839846 1.4160156,0 0,1.4160156 8.5839844,10 0,18.583985 1.4160156,20 10,11.416015 18.583984,20 20,18.583985 Z
m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z
m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z
+
+
+
+
+
+
+
@@ -33,7 +40,7 @@
-
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/Window.xaml b/src/Avalonia.Themes.Fluent/Controls/Window.xaml
index fc71798d97..90963c606c 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Window.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Window.xaml
@@ -2,7 +2,7 @@
-
+
diff --git a/src/Avalonia.Themes.Fluent/FluentDark.xaml b/src/Avalonia.Themes.Fluent/FluentDark.xaml
index 74b583a240..50b12efd78 100644
--- a/src/Avalonia.Themes.Fluent/FluentDark.xaml
+++ b/src/Avalonia.Themes.Fluent/FluentDark.xaml
@@ -1,9 +1,9 @@
+ xmlns:sys="clr-namespace:System;assembly=netstandard">
+
-
diff --git a/src/Avalonia.Themes.Fluent/FluentLight.xaml b/src/Avalonia.Themes.Fluent/FluentLight.xaml
index 1bc51f655e..feb043c5f3 100644
--- a/src/Avalonia.Themes.Fluent/FluentLight.xaml
+++ b/src/Avalonia.Themes.Fluent/FluentLight.xaml
@@ -1,9 +1,9 @@
+
-
diff --git a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs
new file mode 100644
index 0000000000..508891fd72
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using Avalonia.Logging;
+using Avalonia.Media;
+
+namespace Avalonia.Animation.Animators
+{
+ ///
+ /// Animator that handles all animations on properties
+ /// with as their type and
+ /// redirect them to the properly registered
+ /// animators in this class.
+ ///
+ public class BaseBrushAnimator : Animator
+ {
+ private IAnimator _targetAnimator;
+
+ private static readonly List<(Func Match, Type AnimatorType)> _brushAnimators =
+ new List<(Func Match, Type AnimatorType)>();
+
+ ///
+ /// Register an that handles a specific
+ /// 's descendant value type.
+ ///
+ ///
+ /// The condition to which the
+ /// is to be activated and used.
+ ///
+ ///
+ /// The type of the animator to instantiate.
+ ///
+ public static void RegisterBrushAnimator(Func condition)
+ where TAnimator : IAnimator
+ {
+ _brushAnimators.Insert(0, (condition, typeof(TAnimator)));
+ }
+
+ ///
+ public override IDisposable Apply(Animation animation, Animatable control, IClock clock,
+ IObservable match, Action onComplete)
+ {
+ foreach (var valueType in _brushAnimators)
+ {
+ if (!valueType.Match(this[0].Value.GetType())) continue;
+
+ _targetAnimator = (IAnimator)Activator.CreateInstance(valueType.AnimatorType);
+
+ foreach (var keyframe in this)
+ {
+ _targetAnimator.Add(keyframe);
+ }
+
+ _targetAnimator.Property = this.Property;
+
+ return _targetAnimator.Apply(animation, control, clock, match, onComplete);
+ }
+
+ Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log(
+ this,
+ "The animation's keyframe values didn't match any brush animators registered in BaseBrushAnimator.");
+
+ return Disposable.Empty;
+ }
+
+ ///
+ public override IBrush Interpolate(double progress, IBrush oldValue, IBrush newValue) => null;
+ }
+}
diff --git a/src/Avalonia.Visuals/Animation/Animators/ColorAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/ColorAnimator.cs
index 6d1f6c39bd..1686cafeb9 100644
--- a/src/Avalonia.Visuals/Animation/Animators/ColorAnimator.cs
+++ b/src/Avalonia.Visuals/Animation/Animators/ColorAnimator.cs
@@ -31,6 +31,11 @@ namespace Avalonia.Animation.Animators
}
public override Color Interpolate(double progress, Color oldValue, Color newValue)
+ {
+ return InterpolateCore(progress, oldValue, newValue);
+ }
+
+ internal static Color InterpolateCore(double progress, Color oldValue, Color newValue)
{
// normalize sRGB values.
var oldA = oldValue.A / 255d;
@@ -59,7 +64,7 @@ namespace Avalonia.Animation.Animators
var b = oldB + progress * (newB - oldB);
// convert back to sRGB in the [0..255] range
- a = a * 255d;
+ a *= 255d;
r = OECF_sRGB(r) * 255d;
g = OECF_sRGB(g) * 255d;
b = OECF_sRGB(b) * 255d;
diff --git a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs
index a8e618af27..a56cc1de8c 100644
--- a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs
+++ b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs
@@ -1,71 +1,42 @@
using System;
-using System.Reactive.Disposables;
+using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Media.Immutable;
namespace Avalonia.Animation.Animators
{
///
- /// Animator that handles .
+ /// Animator that handles values.
///
- public class SolidColorBrushAnimator : Animator
+ public class ISolidColorBrushAnimator : Animator
{
- private ColorAnimator _colorAnimator;
-
- private void InitializeColorAnimator()
+ public override ISolidColorBrush Interpolate(double progress, ISolidColorBrush oldValue, ISolidColorBrush newValue)
{
- _colorAnimator = new ColorAnimator();
-
- foreach (AnimatorKeyFrame keyframe in this)
+ if (oldValue is null || newValue is null)
{
- _colorAnimator.Add(keyframe);
+ return oldValue;
}
- _colorAnimator.Property = SolidColorBrush.ColorProperty;
+ return new ImmutableSolidColorBrush(ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color));
}
- public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete)
+ public override IDisposable BindAnimation(Animatable control, IObservable instance)
{
- // Preprocess keyframe values to Color if the xaml parser converts them to ISCB.
- foreach (var keyframe in this)
- {
- if (keyframe.Value is ISolidColorBrush colorBrush)
- {
- keyframe.Value = colorBrush.Color;
- }
- else
- {
- return Disposable.Empty;
- }
- }
-
- SolidColorBrush finalTarget;
- var targetVal = control.GetValue(Property);
- if (targetVal is null)
- {
- finalTarget = new SolidColorBrush(Colors.Transparent);
- control.SetValue(Property, finalTarget);
- }
- else if (targetVal is ImmutableSolidColorBrush immutableSolidColorBrush)
- {
- finalTarget = new SolidColorBrush(immutableSolidColorBrush.Color);
- control.SetValue(Property, finalTarget);
- }
- else if (targetVal is ISolidColorBrush)
- {
- finalTarget = targetVal as SolidColorBrush;
- }
- else
+ return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation);
+ }
+ }
+
+ [Obsolete]
+ public class SolidColorBrushAnimator : Animator
+ {
+ public override SolidColorBrush Interpolate(double progress, SolidColorBrush oldValue, SolidColorBrush newValue)
+ {
+ if (oldValue is null || newValue is null)
{
- return Disposable.Empty;
+ return oldValue;
}
- if (_colorAnimator == null)
- InitializeColorAnimator();
-
- return _colorAnimator.Apply(animation, finalTarget, clock ?? control.Clock, match, onComplete);
+ return new SolidColorBrush(ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color));
}
-
- public override SolidColorBrush Interpolate(double p, SolidColorBrush o, SolidColorBrush n) => null;
}
}
diff --git a/src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs
new file mode 100644
index 0000000000..8a070836e9
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs
@@ -0,0 +1,12 @@
+using Avalonia.Animation.Animators;
+using Avalonia.Media;
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Transition class that handles with type.
+ ///
+ public class BoxShadowsTransition : AnimatorDrivenTransition
+ {
+ }
+}
diff --git a/src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs
new file mode 100644
index 0000000000..9b925a3779
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs
@@ -0,0 +1,12 @@
+using Avalonia.Animation.Animators;
+using Avalonia.Media;
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Transition class that handles with type.
+ ///
+ public class ColorTransition : AnimatorDrivenTransition
+ {
+ }
+}
diff --git a/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs
index 0b0f04ca94..62b77a64e1 100644
--- a/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs
+++ b/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs
@@ -1,33 +1,11 @@
-using System;
-using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
///
/// Transition class that handles with type.
///
- public class CornerRadiusTransition : Transition
+ public class CornerRadiusTransition : AnimatorDrivenTransition
{
- ///
- public override IObservable DoTransition(IObservable progress, CornerRadius oldValue, CornerRadius newValue)
- {
- return progress
- .Select(p =>
- {
- var f = Easing.Ease(p);
-
- var deltaTL = newValue.TopLeft - oldValue.TopLeft;
- var deltaTR = newValue.TopRight - oldValue.TopRight;
- var deltaBR = newValue.BottomRight - oldValue.BottomRight;
- var deltaBL = newValue.BottomLeft - oldValue.BottomLeft;
-
- var nTL = f * deltaTL + oldValue.TopLeft;
- var nTR = f * deltaTR + oldValue.TopRight;
- var nBR = f * deltaBR + oldValue.BottomRight;
- var nBL = f * deltaBL + oldValue.BottomLeft;
-
- return new CornerRadius(nTL, nTR, nBR, nBL);
- });
- }
}
}
diff --git a/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs
index 29db5fc868..0985aaa8f8 100644
--- a/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs
+++ b/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs
@@ -1,22 +1,11 @@
-using System;
-using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
///
/// Transition class that handles with type.
///
- public class PointTransition : Transition
+ public class PointTransition : AnimatorDrivenTransition
{
- ///
- public override IObservable DoTransition(IObservable progress, Point oldValue, Point newValue)
- {
- return progress
- .Select(p =>
- {
- var f = Easing.Ease(p);
- return ((newValue - oldValue) * f) + oldValue;
- });
- }
}
}
diff --git a/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs
index b40e789915..6e59fb3631 100644
--- a/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs
+++ b/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs
@@ -1,22 +1,11 @@
-using System;
-using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
///
/// Transition class that handles with type.
///
- public class SizeTransition : Transition
+ public class SizeTransition : AnimatorDrivenTransition
{
- ///
- public override IObservable DoTransition(IObservable progress, Size oldValue, Size newValue)
- {
- return progress
- .Select(p =>
- {
- var f = Easing.Ease(p);
- return ((newValue - oldValue) * f) + oldValue;
- });
- }
}
}
diff --git a/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs
index 28d4ea067f..f50929348f 100644
--- a/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs
+++ b/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs
@@ -1,22 +1,11 @@
-using System;
-using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
///
/// Transition class that handles with type.
///
- public class ThicknessTransition : Transition
+ public class ThicknessTransition : AnimatorDrivenTransition
{
- ///
- public override IObservable DoTransition(IObservable progress, Thickness oldValue, Thickness newValue)
- {
- return progress
- .Select(p =>
- {
- var f = Easing.Ease(p);
- return ((newValue - oldValue) * f) + oldValue;
- });
- }
}
}
diff --git a/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs
index 104acb71ad..73fc23ad99 100644
--- a/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs
+++ b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs
@@ -1,28 +1,26 @@
using System;
-using System.Reactive.Linq;
using Avalonia.Animation.Animators;
using Avalonia.Media;
+using Avalonia.Media.Transformation;
+
+#nullable enable
namespace Avalonia.Animation
{
public class TransformOperationsTransition : Transition
{
- private static readonly TransformOperationsAnimator _operationsAnimator = new TransformOperationsAnimator();
+ private static readonly TransformOperationsAnimator s_operationsAnimator = new TransformOperationsAnimator();
- public override IObservable DoTransition(IObservable progress,
+ public override IObservable DoTransition(
+ IObservable progress,
ITransform oldValue,
ITransform newValue)
{
var oldTransform = TransformOperationsAnimator.EnsureOperations(oldValue);
var newTransform = TransformOperationsAnimator.EnsureOperations(newValue);
- return progress
- .Select(p =>
- {
- var f = Easing.Ease(p);
-
- return _operationsAnimator.Interpolate(f, oldTransform, newTransform);
- });
+ return new AnimatorTransitionObservable(
+ s_operationsAnimator, progress, Easing, oldTransform, newTransform);
}
}
}
diff --git a/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs
index c073e8e192..596989fbae 100644
--- a/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs
+++ b/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs
@@ -1,22 +1,11 @@
-using System;
-using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
///
/// Transition class that handles with type.
///
- public class VectorTransition : Transition
+ public class VectorTransition : AnimatorDrivenTransition
{
- ///
- public override IObservable DoTransition(IObservable progress, Vector oldValue, Vector newValue)
- {
- return progress
- .Select(p =>
- {
- var f = Easing.Ease(p);
- return ((newValue - oldValue) * f) + oldValue;
- });
- }
}
}
diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt
new file mode 100644
index 0000000000..f9fd125615
--- /dev/null
+++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt
@@ -0,0 +1,71 @@
+Compat issues with assembly Avalonia.Visuals:
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract.
+CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
+CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextFormatting.TextRunProperties Avalonia.Media.TextFormatting.GenericTextParagraphProperties.DefaultTextRunProperties' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public System.Double Avalonia.Media.TextFormatting.GenericTextParagraphProperties.LineHeight' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextAlignment Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextAlignment' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextWrapping Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextWrapping' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextFormatting.TextRunProperties Avalonia.Media.TextFormatting.GenericTextParagraphProperties.DefaultTextRunProperties.get()' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public System.Double Avalonia.Media.TextFormatting.GenericTextParagraphProperties.LineHeight.get()' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextAlignment Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextAlignment.get()' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextWrapping Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextWrapping.get()' is non-virtual in the implementation but is virtual in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.GenericTextRunProperties..ctor(Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextDecorationCollection, Avalonia.Media.IBrush, Avalonia.Media.IBrush, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextEndOfLine..ctor()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Height' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.NewLineLength' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangAfter' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangLeading' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangTrailing' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Start' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.TrailingWhitespaceLength' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Width' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.WidthIncludingTrailingWhitespace' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline.get()' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Height.get()' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLine.LineMetrics.get()' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.NewLineLength.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangAfter.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangLeading.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangTrailing.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Start.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.TrailingWhitespaceLength.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Width.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.WidthIncludingTrailingWhitespace.get()' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLineMetrics..ctor(Avalonia.Size, System.Double, Avalonia.Media.TextFormatting.TextRange, System.Boolean)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLineMetrics.Create(System.Collections.Generic.IEnumerable, Avalonia.Media.TextFormatting.TextRange, System.Double, Avalonia.Media.TextFormatting.TextParagraphProperties)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLineMetrics.Size.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextRange Avalonia.Media.TextFormatting.TextLineMetrics.TextRange.get()' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextParagraphProperties.FirstLineInParagraph' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public Avalonia.Media.FlowDirection Avalonia.Media.TextFormatting.TextParagraphProperties.FlowDirection' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextParagraphProperties.Indent' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextParagraphProperties.FirstLineInParagraph.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public Avalonia.Media.FlowDirection Avalonia.Media.TextFormatting.TextParagraphProperties.FlowDirection.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextParagraphProperties.Indent.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment.get()' is abstract in the implementation but is missing in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PopBitmapBlendMode()' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PushBitmapBlendMode(Avalonia.Visuals.Media.Imaging.BitmapBlendingMode)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength.get()' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAndTangentAtDistance(System.Double, Avalonia.Point, Avalonia.Point)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAtDistance(System.Double, Avalonia.Point)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetSegment(System.Double, System.Double, System.Boolean, Avalonia.Platform.IGeometryImpl)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' does not exist in the implementation but it does exist in the contract.
+Total Issues: 64
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.IO.Stream)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract.
+Total Issues: 11
diff --git a/src/Avalonia.Visuals/Media/BaselineAlignment.cs b/src/Avalonia.Visuals/Media/BaselineAlignment.cs
new file mode 100644
index 0000000000..71759003fa
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/BaselineAlignment.cs
@@ -0,0 +1,32 @@
+namespace Avalonia.Media
+{
+ ///
+ /// Enum specifying where a box should be positioned Vertically
+ ///
+ public enum BaselineAlignment
+ {
+ /// Align top toward top of container
+ Top,
+
+ /// Center vertically
+ Center,
+
+ /// Align bottom toward bottom of container
+ Bottom,
+
+ /// Align at baseline
+ Baseline,
+
+ /// Align toward text's top of container
+ TextTop,
+
+ /// Align toward text's bottom of container
+ TextBottom,
+
+ /// Align baseline to subscript position of container
+ Subscript,
+
+ /// Align baseline to superscript position of container
+ Superscript,
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Brush.cs b/src/Avalonia.Visuals/Media/Brush.cs
index a19d5af8b7..fb03d19a4e 100644
--- a/src/Avalonia.Visuals/Media/Brush.cs
+++ b/src/Avalonia.Visuals/Media/Brush.cs
@@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using Avalonia.Animation;
+using Avalonia.Animation.Animators;
namespace Avalonia.Media
{
@@ -21,6 +22,7 @@ namespace Avalonia.Media
static Brush()
{
+ Animation.Animation.RegisterAnimator(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType));
AffectsRender(OpacityProperty);
}
diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs
index ae4c927ae2..4e3dc8699c 100644
--- a/src/Avalonia.Visuals/Media/DrawingContext.cs
+++ b/src/Avalonia.Visuals/Media/DrawingContext.cs
@@ -121,12 +121,23 @@ namespace Avalonia.Media
/// The stroke pen.
/// The geometry.
public void DrawGeometry(IBrush brush, IPen pen, Geometry geometry)
+ {
+ DrawGeometry(brush, pen, geometry.PlatformImpl);
+ }
+
+ ///
+ /// Draws a geometry.
+ ///
+ /// The fill brush.
+ /// The stroke pen.
+ /// The geometry.
+ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
{
Contract.Requires(geometry != null);
if (brush != null || PenIsVisible(pen))
{
- PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl);
+ PlatformImpl.DrawGeometry(brush, pen, geometry);
}
}
diff --git a/src/Avalonia.Visuals/Media/FlowDirection.cs b/src/Avalonia.Visuals/Media/FlowDirection.cs
new file mode 100644
index 0000000000..45684907b1
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/FlowDirection.cs
@@ -0,0 +1,25 @@
+namespace Avalonia.Media
+{
+ ///
+ /// The 'flow-direction' property specifies whether the primary text advance
+ /// direction shall be left-to-right or right-to-left.
+ ///
+ public enum FlowDirection
+ {
+ ///
+ /// Sets the primary text advance direction to left-to-right, and the line
+ /// progression direction to top-to-bottom as is common in most Roman-based
+ /// documents. For most characters, the current text position is advanced
+ /// from left to right after each glyph is rendered. The 'direction' property
+ /// is set to 'ltr'.
+ ///
+ LeftToRight,
+
+ ///
+ /// Sets the primary text advance direction to right-to-left, and the line
+ /// progression direction to top-to-bottom as is common in Arabic or Hebrew
+ /// scripts. The direction property is set to 'rtl'.
+ ///
+ RightToLeft
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs
index af228ec57b..2b787462e4 100644
--- a/src/Avalonia.Visuals/Media/GlyphRun.cs
+++ b/src/Avalonia.Visuals/Media/GlyphRun.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
@@ -16,9 +17,9 @@ namespace Avalonia.Media
private IGlyphRunImpl _glyphRunImpl;
private GlyphTypeface _glyphTypeface;
private double _fontRenderingEmSize;
- private Size? _size;
private int _biDiLevel;
private Point? _baselineOrigin;
+ private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlySlice _glyphIndices;
private ReadOnlySlice _glyphAdvances;
@@ -90,6 +91,24 @@ namespace Avalonia.Media
set => Set(ref _fontRenderingEmSize, value);
}
+ ///
+ /// Gets or sets the conservative bounding box of the .
+ ///
+ public Size Size => new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height);
+
+ ///
+ ///
+ ///
+ public GlyphRunMetrics Metrics
+ {
+ get
+ {
+ _glyphRunMetrics ??= CreateGlyphRunMetrics();
+
+ return _glyphRunMetrics.Value;
+ }
+ }
+
///
/// Gets or sets the baseline origin of the.
///
@@ -168,19 +187,6 @@ namespace Avalonia.Media
///
public bool IsLeftToRight => ((BiDiLevel & 1) == 0);
- ///
- /// Gets or sets the conservative bounding box of the .
- ///
- public Size Size
- {
- get
- {
- _size ??= CalculateSize();
-
- return _size.Value;
- }
- }
-
///
/// The platform implementation of the .
///
@@ -232,16 +238,7 @@ namespace Avalonia.Media
for (var i = 0; i < glyphIndex; i++)
{
- if (GlyphAdvances.IsEmpty)
- {
- var glyph = GlyphIndices[i];
-
- distance += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
- }
- else
- {
- distance += GlyphAdvances[i];
- }
+ distance += GetGlyphAdvance(i);
}
return distance;
@@ -282,42 +279,20 @@ namespace Avalonia.Media
var currentX = 0.0;
var index = 0;
- if (GlyphTypeface.IsFixedPitch)
+ for (; index < GlyphIndices.Length - Metrics.NewlineLength; index++)
{
- var glyph = GlyphIndices[index];
+ var advance = GetGlyphAdvance(index);
- var advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
-
- index = Math.Min(GlyphIndices.Length - 1,
- (int)Math.Round(distance / advance, MidpointRounding.AwayFromZero));
- }
- else
- {
- for (; index < GlyphIndices.Length; index++)
+ if (currentX + advance >= distance)
{
- double advance;
-
- if (GlyphAdvances.IsEmpty)
- {
- var glyph = GlyphIndices[index];
-
- advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
- }
- else
- {
- advance = GlyphAdvances[index];
- }
-
- if (currentX + advance >= distance)
- {
- break;
- }
-
- currentX += advance;
+ break;
}
+
+ currentX += advance;
}
- var characterHit = FindNearestCharacterHit(GlyphClusters.IsEmpty ? index : GlyphClusters[index], out var width);
+ var characterHit =
+ FindNearestCharacterHit(GlyphClusters.IsEmpty ? index : GlyphClusters[index], out var width);
var offset = GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex));
@@ -343,7 +318,8 @@ namespace Avalonia.Media
return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
}
- var nextCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
+ var nextCharacterHit =
+ FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
return new CharacterHit(nextCharacterHit.FirstCharacterIndex);
}
@@ -368,14 +344,6 @@ namespace Avalonia.Media
FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
}
- private class ReverseComparer : IComparer
- {
- public int Compare(T x, T y)
- {
- return Comparer.Default.Compare(y, x);
- }
- }
-
///
/// Finds a glyph index for given character index.
///
@@ -472,7 +440,7 @@ namespace Avalonia.Media
if (GlyphClusters.IsEmpty)
{
- width = GetGlyphWidth(index);
+ width = GetGlyphAdvance(index);
return new CharacterHit(start, 1);
}
@@ -485,7 +453,7 @@ namespace Avalonia.Media
while (nextCluster == cluster)
{
- width += GetGlyphWidth(currentIndex);
+ width += GetGlyphAdvance(currentIndex);
if (IsLeftToRight)
{
@@ -528,16 +496,16 @@ namespace Avalonia.Media
///
/// The glyph index.
/// The glyph's width.
- private double GetGlyphWidth(int index)
+ private double GetGlyphAdvance(int index)
{
- if (GlyphAdvances.IsEmpty)
+ if (!GlyphAdvances.IsEmpty)
{
- var glyph = GlyphIndices[index];
-
- return GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ return GlyphAdvances[index];
}
- return GlyphAdvances[index];
+ var glyph = GlyphIndices[index];
+
+ return GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
///
@@ -549,34 +517,90 @@ namespace Avalonia.Media
return new Point(0, -GlyphTypeface.Ascent * Scale);
}
- ///
- /// Calculates the size of the .
- ///
- ///
- /// The calculated bounds.
- ///
- private Size CalculateSize()
+ private GlyphRunMetrics CreateGlyphRunMetrics()
{
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
- var width = 0.0;
+ var widthIncludingTrailingWhitespace = 0d;
+ var width = 0d;
+
+ var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength);
- if (GlyphAdvances.IsEmpty)
+ for (var index = 0; index < _glyphIndices.Length; index++)
{
- foreach (var glyph in GlyphIndices)
+ var advance = GetGlyphAdvance(index);
+
+ widthIncludingTrailingWhitespace += advance;
+
+ if (index > _glyphIndices.Length - 1 - trailingWhitespaceLength)
{
- width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ continue;
+ }
+
+ width += advance;
+ }
+
+ return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength,
+ height);
+ }
+
+ private int GetTrailingWhitespaceLength(out int newLineLength)
+ {
+ newLineLength = 0;
+
+ if (_characters.IsEmpty)
+ {
+ return 0;
+ }
+
+ var trailingWhitespaceLength = 0;
+
+ if (_glyphClusters.IsEmpty)
+ {
+ for (var i = _characters.Length - 1; i >= 0;)
+ {
+ var codepoint = Codepoint.ReadAt(_characters, i, out var count);
+
+ if (!codepoint.IsWhiteSpace)
+ {
+ break;
+ }
+
+ if (codepoint.IsBreakChar)
+ {
+ newLineLength++;
+ }
+
+ trailingWhitespaceLength++;
+
+ i -= count;
}
}
else
{
- foreach (var advance in GlyphAdvances)
+ for (var i = _glyphClusters.Length - 1; i >= 0; i--)
{
- width += advance;
+ var cluster = _glyphClusters[i];
+
+ var codepointIndex = cluster - _characters.Start;
+
+ var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
+
+ if (!codepoint.IsWhiteSpace)
+ {
+ break;
+ }
+
+ if (codepoint.IsBreakChar)
+ {
+ newLineLength++;
+ }
+
+ trailingWhitespaceLength++;
}
}
- return new Size(width, height);
+ return trailingWhitespaceLength;
}
private void Set(ref T field, T value)
@@ -586,6 +610,10 @@ namespace Avalonia.Media
throw new InvalidOperationException("GlyphRun can't be changed after it has been initialized.'");
}
+ _glyphRunMetrics = null;
+
+ _baselineOrigin = null;
+
field = value;
}
@@ -613,16 +641,20 @@ namespace Avalonia.Media
var platformRenderInterface = AvaloniaLocator.Current.GetService();
- _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width);
-
- var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
-
- _size = new Size(width, height);
+ _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this);
}
void IDisposable.Dispose()
{
_glyphRunImpl?.Dispose();
}
+
+ private class ReverseComparer : IComparer
+ {
+ public int Compare(T x, T y)
+ {
+ return Comparer.Default.Compare(y, x);
+ }
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/GlyphRunMetrics.cs b/src/Avalonia.Visuals/Media/GlyphRunMetrics.cs
new file mode 100644
index 0000000000..a8698a7d82
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/GlyphRunMetrics.cs
@@ -0,0 +1,25 @@
+namespace Avalonia.Media
+{
+ public readonly struct GlyphRunMetrics
+ {
+ public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, int trailingWhitespaceLength,
+ int newlineLength, double height)
+ {
+ Width = width;
+ WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
+ TrailingWhitespaceLength = trailingWhitespaceLength;
+ NewlineLength = newlineLength;
+ Height = height;
+ }
+
+ public double Width { get; }
+
+ public double WidthIncludingTrailingWhitespace { get; }
+
+ public int TrailingWhitespaceLength { get; }
+
+ public int NewlineLength { get; }
+
+ public double Height { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
index 35aa62aa65..2be505b9c0 100644
--- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs
+++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
@@ -5,6 +5,8 @@ namespace Avalonia.Media
{
public sealed class GlyphTypeface : IDisposable
{
+ public const int InvisibleGlyph = 3;
+
public GlyphTypeface(Typeface typeface)
: this(FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface))
{
diff --git a/src/Avalonia.Visuals/Media/Imaging/BitmapBlendingMode.cs b/src/Avalonia.Visuals/Media/Imaging/BitmapBlendingMode.cs
new file mode 100644
index 0000000000..473b43dab3
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/Imaging/BitmapBlendingMode.cs
@@ -0,0 +1,57 @@
+namespace Avalonia.Visuals.Media.Imaging
+{
+ ///
+ /// Controls the way the bitmaps are drawn together.
+ ///
+ public enum BitmapBlendingMode
+ {
+ ///
+ /// Source is placed over the destination.
+ ///
+ SourceOver,
+ ///
+ /// Only the source will be present.
+ ///
+ Source,
+ ///
+ /// Only the destination will be present.
+ ///
+ Destination,
+ ///
+ /// Destination is placed over the source.
+ ///
+ DestinationOver,
+ ///
+ /// The source that overlaps the destination, replaces the destination.
+ ///
+ SourceIn,
+ ///
+ /// Destination which overlaps the source, replaces the source.
+ ///
+ DestinationIn,
+ ///
+ /// Source is placed, where it falls outside of the destination.
+ ///
+ SourceOut,
+ ///
+ /// Destination is placed, where it falls outside of the source.
+ ///
+ DestinationOut,
+ ///
+ /// Source which overlaps the destination, replaces the destination.
+ ///
+ SourceAtop,
+ ///
+ /// Destination which overlaps the source replaces the source.
+ ///
+ DestinationAtop,
+ ///
+ /// The non-overlapping regions of source and destination are combined.
+ ///
+ Xor,
+ ///
+ /// Display the sum of the source image and destination image.
+ ///
+ Plus,
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs
index b86444bc2f..b0e1a954e6 100644
--- a/src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs
+++ b/src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs
@@ -1,5 +1,8 @@
using System;
+using System.IO;
+using System.Threading.Tasks;
using Avalonia.Platform;
+using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Media.Imaging
{
@@ -34,8 +37,50 @@ namespace Avalonia.Media.Imaging
{
}
+ private WriteableBitmap(IWriteableBitmapImpl impl) : base(impl)
+ {
+
+ }
+
public ILockedFramebuffer Lock() => ((IWriteableBitmapImpl) PlatformImpl.Item).Lock();
+ public static WriteableBitmap Decode(Stream stream)
+ {
+ var ri = AvaloniaLocator.Current.GetService();
+
+ return new WriteableBitmap(ri.LoadWriteableBitmap(stream));
+ }
+
+ ///
+ /// Loads a WriteableBitmap from a stream and decodes at the desired width. Aspect ratio is maintained.
+ /// This is more efficient than loading and then resizing.
+ ///
+ /// The stream to read the bitmap from. This can be any supported image format.
+ /// The desired width of the resulting bitmap.
+ /// The to use should any scaling be required.
+ /// An instance of the class.
+ public new static WriteableBitmap DecodeToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ var ri = AvaloniaLocator.Current.GetService();
+
+ return new WriteableBitmap(ri.LoadWriteableBitmapToWidth(stream, width, interpolationMode));
+ }
+
+ ///
+ /// Loads a Bitmap from a stream and decodes at the desired height. Aspect ratio is maintained.
+ /// This is more efficient than loading and then resizing.
+ ///
+ /// The stream to read the bitmap from. This can be any supported image format.
+ /// The desired height of the resulting bitmap.
+ /// The to use should any scaling be required.
+ /// An instance of the class.
+ public new static WriteableBitmap DecodeToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ var ri = AvaloniaLocator.Current.GetService();
+
+ return new WriteableBitmap(ri.LoadWriteableBitmapToHeight(stream, height, interpolationMode));
+ }
+
private static IBitmapImpl CreatePlatformImpl(PixelSize size, in Vector dpi, PixelFormat? format, AlphaFormat? alphaFormat)
{
var ri = AvaloniaLocator.Current.GetService();
diff --git a/src/Avalonia.Visuals/Media/SolidColorBrush.cs b/src/Avalonia.Visuals/Media/SolidColorBrush.cs
index 8e30880489..fd94cbd214 100644
--- a/src/Avalonia.Visuals/Media/SolidColorBrush.cs
+++ b/src/Avalonia.Visuals/Media/SolidColorBrush.cs
@@ -1,4 +1,3 @@
-using Avalonia.Animation;
using Avalonia.Animation.Animators;
using Avalonia.Media.Immutable;
@@ -17,7 +16,7 @@ namespace Avalonia.Media
static SolidColorBrush()
{
- Animation.Animation.RegisterAnimator(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType));
+ BaseBrushAnimator.RegisterBrushAnimator(match => typeof(ISolidColorBrush).IsAssignableFrom(match));
AffectsRender(ColorProperty);
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
index 338c92f6b1..3757a4506a 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
@@ -14,6 +14,7 @@
/// Draws the at the given origin.
///
/// The drawing context.
- public abstract void Draw(DrawingContext drawingContext);
+ /// The origin.
+ public abstract void Draw(DrawingContext drawingContext, Point origin);
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs
index 8e7d934bca..dccad1e647 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs
@@ -1,33 +1,144 @@
namespace Avalonia.Media.TextFormatting
{
- public class GenericTextParagraphProperties : TextParagraphProperties
+ ///
+ /// Generic implementation of TextParagraphProperties
+ ///
+ public sealed class GenericTextParagraphProperties : TextParagraphProperties
{
+ private FlowDirection _flowDirection;
private TextAlignment _textAlignment;
- private TextWrapping _textWrapping;
+ private TextWrapping _textWrap;
private double _lineHeight;
- public GenericTextParagraphProperties(
- TextRunProperties defaultTextRunProperties,
+ ///
+ /// Constructing TextParagraphProperties
+ ///
+ /// default paragraph's default run properties
+ /// logical horizontal alignment
+ /// text wrap option
+ /// Paragraph line height
+ public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties,
TextAlignment textAlignment = TextAlignment.Left,
- TextWrapping textWrapping = TextWrapping.NoWrap,
+ TextWrapping textWrap = TextWrapping.NoWrap,
double lineHeight = 0)
{
DefaultTextRunProperties = defaultTextRunProperties;
+ _textAlignment = textAlignment;
+ _textWrap = textWrap;
+ _lineHeight = lineHeight;
+ }
+ ///
+ /// Constructing TextParagraphProperties
+ ///
+ /// text flow direction
+ /// logical horizontal alignment
+ /// true if the paragraph is the first line in the paragraph
+ /// true if the line is always collapsible
+ /// default paragraph's default run properties
+ /// text wrap option
+ /// Paragraph line height
+ /// line indentation
+ public GenericTextParagraphProperties(
+ FlowDirection flowDirection,
+ TextAlignment textAlignment,
+ bool firstLineInParagraph,
+ bool alwaysCollapsible,
+ TextRunProperties defaultTextRunProperties,
+ TextWrapping textWrap,
+ double lineHeight,
+ double indent
+ )
+ {
+ _flowDirection = flowDirection;
_textAlignment = textAlignment;
+ FirstLineInParagraph = firstLineInParagraph;
+ AlwaysCollapsible = alwaysCollapsible;
+ DefaultTextRunProperties = defaultTextRunProperties;
+ _textWrap = textWrap;
+ _lineHeight = lineHeight;
+ Indent = indent;
+ }
- _textWrapping = textWrapping;
+ ///
+ /// Constructing TextParagraphProperties from another one
+ ///
+ /// source line props
+ public GenericTextParagraphProperties(TextParagraphProperties textParagraphProperties)
+ : this(textParagraphProperties.FlowDirection,
+ textParagraphProperties.TextAlignment,
+ textParagraphProperties.FirstLineInParagraph,
+ textParagraphProperties.AlwaysCollapsible,
+ textParagraphProperties.DefaultTextRunProperties,
+ textParagraphProperties.TextWrapping,
+ textParagraphProperties.LineHeight,
+ textParagraphProperties.Indent)
+ {
+ }
- _lineHeight = lineHeight;
+ ///
+ /// This property specifies whether the primary text advance
+ /// direction shall be left-to-right, right-to-left, or top-to-bottom.
+ ///
+ public override FlowDirection FlowDirection
+ {
+ get { return _flowDirection; }
}
+ ///
+ /// This property describes how inline content of a block is aligned.
+ ///
+ public override TextAlignment TextAlignment
+ {
+ get { return _textAlignment; }
+ }
+
+ ///
+ /// Paragraph's line height
+ ///
+ public override double LineHeight
+ {
+ get { return _lineHeight; }
+ }
+
+ ///
+ /// Indicates the first line of the paragraph.
+ ///
+ public override bool FirstLineInParagraph { get; }
+
+ ///
+ /// If true, the formatted line may always be collapsed. If false (the default),
+ /// only lines that overflow the paragraph width are collapsed.
+ ///
+ public override bool AlwaysCollapsible { get; }
+
+ ///
+ /// Paragraph's default run properties
+ ///
public override TextRunProperties DefaultTextRunProperties { get; }
- public override TextAlignment TextAlignment => _textAlignment;
+ ///
+ /// This property controls whether or not text wraps when it reaches the flow edge
+ /// of its containing block box
+ ///
+ public override TextWrapping TextWrapping
+ {
+ get { return _textWrap; }
+ }
+
+ ///
+ /// Line indentation
+ ///
+ public override double Indent { get; }
- public override TextWrapping TextWrapping => _textWrapping;
+ ///
+ /// Set text flow direction
+ ///
+ internal void SetFlowDirection(FlowDirection flowDirection)
+ {
+ _flowDirection = flowDirection;
+ }
- public override double LineHeight => _lineHeight;
///
/// Set text alignment
@@ -37,20 +148,21 @@
_textAlignment = textAlignment;
}
+
///
- /// Set text wrap
+ /// Set line height
///
- internal void SetTextWrapping(TextWrapping textWrapping)
+ internal void SetLineHeight(double lineHeight)
{
- _textWrapping = textWrapping;
+ _lineHeight = lineHeight;
}
///
- /// Set line height
+ /// Set text wrap
///
- internal void SetLineHeight(double lineHeight)
+ internal void SetTextWrapping(TextWrapping textWrap)
{
- _lineHeight = lineHeight;
+ _textWrap = textWrap;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs
index 3db3589498..7756ca5c8f 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs
@@ -7,8 +7,11 @@ namespace Avalonia.Media.TextFormatting
///
public class GenericTextRunProperties : TextRunProperties
{
- public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize = 12,
- TextDecorationCollection textDecorations = null, IBrush foregroundBrush = null, IBrush backgroundBrush = null,
+ private const double DefaultFontRenderingEmSize = 12;
+
+ public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize = DefaultFontRenderingEmSize,
+ TextDecorationCollection textDecorations = null, IBrush foregroundBrush = null,
+ IBrush backgroundBrush = null, BaselineAlignment baselineAlignment = BaselineAlignment.Baseline,
CultureInfo cultureInfo = null)
{
Typeface = typeface;
@@ -16,6 +19,7 @@ namespace Avalonia.Media.TextFormatting
TextDecorations = textDecorations;
ForegroundBrush = foregroundBrush;
BackgroundBrush = backgroundBrush;
+ BaselineAlignment = baselineAlignment;
CultureInfo = cultureInfo;
}
@@ -34,6 +38,9 @@ namespace Avalonia.Media.TextFormatting
///
public override IBrush BackgroundBrush { get; }
+ ///
+ public override BaselineAlignment BaselineAlignment { get; }
+
///
public override CultureInfo CultureInfo { get; }
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/LogicalDirection.cs b/src/Avalonia.Visuals/Media/TextFormatting/LogicalDirection.cs
new file mode 100644
index 0000000000..5ff0d0c38f
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/LogicalDirection.cs
@@ -0,0 +1,14 @@
+namespace Avalonia.Media.TextFormatting
+{
+ public enum LogicalDirection
+ {
+ ///
+ /// Backward, or from right to left.
+ ///
+ Backward,
+ ///
+ /// Forward, or from left to right.
+ ///
+ Forward
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
index 9f6f2b2f43..b304b19910 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
@@ -45,38 +45,41 @@ namespace Avalonia.Media.TextFormatting
public GlyphRun GlyphRun { get; }
///
- public override void Draw(DrawingContext drawingContext)
+ public override void Draw(DrawingContext drawingContext, Point origin)
{
- if (GlyphRun.GlyphIndices.Length == 0)
+ using (drawingContext.PushPostTransform(Matrix.CreateTranslation(origin)))
{
- return;
- }
-
- if (Properties.Typeface == default)
- {
- return;
- }
-
- if (Properties.ForegroundBrush == null)
- {
- return;
- }
-
- if (Properties.BackgroundBrush != null)
- {
- drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size));
- }
-
- drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun);
-
- if (Properties.TextDecorations == null)
- {
- return;
- }
-
- foreach (var textDecoration in Properties.TextDecorations)
- {
- textDecoration.Draw(drawingContext, this);
+ if (GlyphRun.GlyphIndices.Length == 0)
+ {
+ return;
+ }
+
+ if (Properties.Typeface == default)
+ {
+ return;
+ }
+
+ if (Properties.ForegroundBrush == null)
+ {
+ return;
+ }
+
+ if (Properties.BackgroundBrush != null)
+ {
+ drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size));
+ }
+
+ drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun);
+
+ if (Properties.TextDecorations == null)
+ {
+ return;
+ }
+
+ foreach (var textDecoration in Properties.TextDecorations)
+ {
+ textDecoration.Draw(drawingContext, this);
+ }
}
}
@@ -106,7 +109,8 @@ namespace Avalonia.Media.TextFormatting
GlyphRun.GlyphAdvances.Take(glyphCount),
GlyphRun.GlyphOffsets.Take(glyphCount),
GlyphRun.Characters.Take(length),
- GlyphRun.GlyphClusters.Take(glyphCount));
+ GlyphRun.GlyphClusters.Take(glyphCount),
+ GlyphRun.BiDiLevel);
var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties);
@@ -117,7 +121,8 @@ namespace Avalonia.Media.TextFormatting
GlyphRun.GlyphAdvances.Skip(glyphCount),
GlyphRun.GlyphOffsets.Skip(glyphCount),
GlyphRun.Characters.Skip(length),
- GlyphRun.GlyphClusters.Skip(glyphCount));
+ GlyphRun.GlyphClusters.Skip(glyphCount),
+ GlyphRun.BiDiLevel);
var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties);
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
index b91a50a27c..0779716ec8 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
@@ -16,6 +16,14 @@ namespace Avalonia.Media.TextFormatting
Properties = properties;
}
+ public TextCharacters(ReadOnlySlice text, int offsetToFirstCharacter, int length,
+ TextRunProperties properties)
+ {
+ Text = text.Skip(offsetToFirstCharacter).Take(length);
+ TextSourceLength = length;
+ Properties = properties;
+ }
+
///
public override int TextSourceLength { get; }
@@ -127,20 +135,20 @@ namespace Avalonia.Media.TextFormatting
count = 0;
var script = Script.Common;
- //var direction = BiDiClass.LeftToRight;
+ var direction = BiDiClass.LeftToRight;
var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface.GlyphTypeface;
-
+
var enumerator = new GraphemeEnumerator(text);
while (enumerator.MoveNext())
{
- var grapheme = enumerator.Current;
+ var currentGrapheme = enumerator.Current;
- var currentScript = grapheme.FirstCodepoint.Script;
+ var currentScript = currentGrapheme.FirstCodepoint.Script;
- //var currentDirection = grapheme.FirstCodepoint.BiDiClass;
+ var currentDirection = currentGrapheme.FirstCodepoint.BiDiClass;
//// ToDo: Implement BiDi algorithm
//if (currentScript.HorizontalDirection != direction)
@@ -153,36 +161,44 @@ namespace Avalonia.Media.TextFormatting
if (currentScript != script)
{
- if (currentScript != Script.Inherited && currentScript != Script.Common)
+ if (script == Script.Inherited || script == Script.Common)
{
- if (script == Script.Inherited || script == Script.Common)
- {
- script = currentScript;
- }
- else
+ script = currentScript;
+ }
+ else
+ {
+ if (currentScript != Script.Inherited && currentScript != Script.Common)
{
break;
}
}
}
- if (isFallback)
+ if (currentScript != Script.Common && currentScript != Script.Inherited)
{
- if (defaultFont.TryGetGlyph(grapheme.FirstCodepoint, out _))
+ if (isFallback && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
break;
}
- }
- if (!font.TryGetGlyph(grapheme.FirstCodepoint, out _))
- {
- if (!grapheme.FirstCodepoint.IsWhiteSpace)
+ if (!font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
break;
}
}
- count += grapheme.Text.Length;
+ if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
+ {
+ break;
+ }
+
+ if (direction == BiDiClass.RightToLeft && currentDirection == BiDiClass.CommonSeparator)
+ {
+ break;
+ }
+
+ count += currentGrapheme.Text.Length;
+ direction = currentDirection;
}
return count > 0;
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs
index fd71fb53e7..21e354a119 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs
@@ -5,5 +5,11 @@
///
public class TextEndOfLine : TextRun
{
+ public TextEndOfLine(int textSourceLength = DefaultTextSourceLength)
+ {
+ TextSourceLength = textSourceLength;
+ }
+
+ public override int TextSourceLength { get; }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
index 6ae5258323..6533c34ba0 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
@@ -12,7 +12,8 @@ namespace Avalonia.Media.TextFormatting
{
var textWrapping = paragraphProperties.TextWrapping;
- var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak);
+ var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak,
+ out var nextLineBreak);
var textRange = GetTextRange(textRuns);
@@ -21,20 +22,18 @@ namespace Avalonia.Media.TextFormatting
switch (textWrapping)
{
case TextWrapping.NoWrap:
- {
- var textLineMetrics =
- TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties);
-
- textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak);
- break;
- }
+ {
+ textLine = new TextLineImpl(textRuns, textRange, paragraphWidth, paragraphProperties,
+ nextLineBreak);
+ break;
+ }
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
- {
- textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties,
- nextLineBreak);
- break;
- }
+ {
+ textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties,
+ nextLineBreak);
+ break;
+ }
default:
throw new ArgumentOutOfRangeException();
}
@@ -51,7 +50,8 @@ namespace Avalonia.Media.TextFormatting
///
/// true if characters fit into the available width; otherwise, false.
///
- internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth, out int count)
+ internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth,
+ out int count)
{
var glyphRun = textCharacters.GlyphRun;
@@ -282,21 +282,24 @@ namespace Avalonia.Media.TextFormatting
switch (textRun)
{
case TextCharacters textCharacters:
- {
- var shapeableRuns = textCharacters.GetShapeableCharacters();
-
- foreach (var run in shapeableRuns)
- {
- var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface,
- run.Properties.FontRenderingEmSize, run.Properties.CultureInfo);
+ {
+ var shapeableRuns = textCharacters.GetShapeableCharacters();
- var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties);
+ foreach (var run in shapeableRuns)
+ {
+ var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface,
+ run.Properties.FontRenderingEmSize, run.Properties.CultureInfo);
- textRuns.Add(shapedCharacters);
- }
+ var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties);
- break;
+ textRuns.Add(shapedCharacters);
}
+
+ break;
+ }
+ case TextEndOfLine textEndOfLine:
+ nextLineBreak = new TextLineBreak(textEndOfLine);
+ break;
}
if (TryGetLineBreak(textRun, out var runLineBreak))
@@ -359,107 +362,137 @@ namespace Avalonia.Media.TextFormatting
{
var availableWidth = paragraphWidth;
var currentWidth = 0.0;
- var runIndex = 0;
- var currentLength = 0;
+ var measuredLength = 0;
- while (runIndex < textRuns.Count)
+ foreach (var currentRun in textRuns)
{
- var currentRun = textRuns[runIndex];
-
if (currentWidth + currentRun.Size.Width > availableWidth)
{
- var breakFound = false;
+ if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var count))
+ {
+ measuredLength += count;
+ }
- var currentBreakPosition = 0;
+ break;
+ }
- if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var measuredLength))
- {
- if (measuredLength < currentRun.Text.Length)
- {
- var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+ currentWidth += currentRun.Size.Width;
- while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
- {
- var nextBreakPosition = lineBreaker.Current.PositionWrap;
+ measuredLength += currentRun.Text.Length;
+ }
- if (nextBreakPosition == 0 || nextBreakPosition > measuredLength)
- {
- break;
- }
+ var currentLength = 0;
- breakFound = lineBreaker.Current.Required ||
- lineBreaker.Current.PositionWrap != currentRun.Text.Length;
+ var lastWrapPosition = 0;
- currentBreakPosition = nextBreakPosition;
- }
- }
- }
- else
+ var currentPosition = 0;
+
+ if (measuredLength == 0 && paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow)
+ {
+ measuredLength = 1;
+ }
+ else
+ {
+ for (var index = 0; index < textRuns.Count; index++)
+ {
+ var currentRun = textRuns[index];
+
+ var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+
+ var breakFound = false;
+
+ while (lineBreaker.MoveNext())
{
- // Make sure we wrap at least one character.
- if (currentLength == 0)
+ if (lineBreaker.Current.Required &&
+ currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
{
- measuredLength = 1;
+ breakFound = true;
+
+ currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+
+ break;
}
- }
- if (breakFound)
- {
- measuredLength = currentBreakPosition;
- }
- else
- {
- if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
+ if ((paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow || lastWrapPosition != 0) &&
+ currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
{
- var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(currentBreakPosition));
-
- if (lineBreaker.MoveNext())
+ if (lastWrapPosition > 0)
{
- measuredLength = currentBreakPosition + lineBreaker.Current.PositionWrap;
+ currentPosition = lastWrapPosition;
}
+ else
+ {
+ currentPosition = currentLength + measuredLength;
+ }
+
+ breakFound = true;
+
+ break;
}
- }
- currentLength += measuredLength;
+ if (currentLength + lineBreaker.Current.PositionWrap >= measuredLength)
+ {
+ currentPosition = currentLength + lineBreaker.Current.PositionWrap;
- var splitResult = SplitTextRuns(textRuns, currentLength);
+ if (index < textRuns.Count - 1 &&
+ lineBreaker.Current.PositionWrap == currentRun.Text.Length)
+ {
+ var nextRun = textRuns[index + 1];
- var textLineMetrics = TextLineMetrics.Create(splitResult.First,
- new TextRange(textRange.Start, currentLength), paragraphWidth, paragraphProperties);
+ lineBreaker = new LineBreakEnumerator(nextRun.Text);
- var remainingCharacters = splitResult.Second;
+ if (lineBreaker.MoveNext() &&
+ lineBreaker.Current.PositionMeasure == 0)
+ {
+ currentPosition += lineBreaker.Current.PositionWrap;
+ }
+ }
- if (currentLineBreak?.RemainingCharacters != null)
- {
- if (remainingCharacters != null)
- {
- remainingCharacters.AddRange(currentLineBreak.RemainingCharacters);
- }
- else
- {
- remainingCharacters = new List(currentLineBreak.RemainingCharacters);
+ breakFound = true;
+
+ break;
}
+
+ lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
+ }
+
+ if (!breakFound)
+ {
+ currentLength += currentRun.Text.Length;
+
+ continue;
}
- var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ?
- new TextLineBreak(remainingCharacters) :
- null;
+ measuredLength = currentPosition;
- return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak);
+ break;
}
+ }
+
+ var splitResult = SplitTextRuns(textRuns, measuredLength);
- currentWidth += currentRun.Size.Width;
+ textRange = new TextRange(textRange.Start, measuredLength);
- currentLength += currentRun.GlyphRun.Characters.Length;
+ var remainingCharacters = splitResult.Second;
- runIndex++;
+ if (currentLineBreak?.RemainingCharacters != null)
+ {
+ if (remainingCharacters != null)
+ {
+ remainingCharacters.AddRange(currentLineBreak.RemainingCharacters);
+ }
+ else
+ {
+ remainingCharacters = new List(currentLineBreak.RemainingCharacters);
+ }
}
- return new TextLineImpl(textRuns,
- TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties),
- currentLineBreak?.RemainingCharacters != null ?
- new TextLineBreak(currentLineBreak.RemainingCharacters) :
- null);
+ var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ?
+ new TextLineBreak(remainingCharacters) :
+ null;
+
+ return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties,
+ lineBreak);
}
///
@@ -545,7 +578,7 @@ namespace Avalonia.Media.TextFormatting
_pos += Current.TextSourceLength;
- return !(Current is TextEndOfLine);
+ return true;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
index daa8807bf6..a4c9392fa0 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
@@ -115,27 +115,165 @@ namespace Avalonia.Media.TextFormatting
/// Draws the text layout.
///
/// The drawing context.
- public void Draw(DrawingContext context)
+ /// The origin.
+ public void Draw(DrawingContext context, Point origin)
{
if (!TextLines.Any())
{
return;
}
+ var (currentX, currentY) = origin;
+
+ foreach (var textLine in TextLines)
+ {
+ textLine.Draw(context, new Point(currentX + textLine.Start, currentY));
+
+ currentY += textLine.Height;
+ }
+ }
+
+ ///
+ /// Get the pixel location relative to the top-left of the layout box given the text position.
+ ///
+ /// The text position.
+ ///
+ public Rect HitTestTextPosition(int textPosition)
+ {
+ if (TextLines.Count == 0)
+ {
+ return new Rect();
+ }
+
+ if (textPosition < 0 || textPosition >= _text.Length)
+ {
+ var lastLine = TextLines[TextLines.Count - 1];
+
+ var lineX = lastLine.Width;
+
+ var lineY = Size.Height - lastLine.Height;
+
+ return new Rect(lineX, lineY, 0, lastLine.Height);
+ }
+
var currentY = 0.0;
foreach (var textLine in TextLines)
{
- var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width,
- _paragraphProperties.TextAlignment);
+ if (textLine.TextRange.End < textPosition)
+ {
+ currentY += textLine.Height;
+
+ continue;
+ }
+
+ var characterHit = new CharacterHit(textPosition);
+
+ var startX = textLine.GetDistanceFromCharacterHit(characterHit);
+
+ var nextCharacterHit = textLine.GetNextCaretCharacterHit(characterHit);
+
+ var endX = textLine.GetDistanceFromCharacterHit(nextCharacterHit);
+
+ return new Rect(startX, currentY, endX - startX, textLine.Height);
+ }
+
+ return new Rect();
+ }
+
+ public IEnumerable HitTestTextRange(int start, int length)
+ {
+ if (start + length <= 0)
+ {
+ return Array.Empty();
+ }
+
+ var result = new List(TextLines.Count);
+
+ var currentY = 0d;
+
+ foreach (var textLine in TextLines)
+ {
+ var currentX = textLine.Start;
+
+ if (textLine.TextRange.End < start)
+ {
+ currentY += textLine.Height;
+
+ continue;
+ }
+
+ if (start > textLine.TextRange.Start)
+ {
+ currentX += textLine.GetDistanceFromCharacterHit(new CharacterHit(start));
+ }
+
+ var endX = textLine.GetDistanceFromCharacterHit(new CharacterHit(start + length));
+
+ result.Add(new Rect(currentX, currentY, endX - currentX, textLine.Height));
+
+ if (textLine.TextRange.Start + textLine.TextRange.Length >= start + length)
+ {
+ break;
+ }
+
+ currentY += textLine.Height;
+ }
+
+ return result;
+ }
+
+ public TextHitTestResult HitTestPoint(in Point point)
+ {
+ var currentY = 0d;
+
+ var lineIndex = 0;
+ TextLine currentLine = null;
+ CharacterHit characterHit;
+
+ for (; lineIndex < TextLines.Count; lineIndex++)
+ {
+ currentLine = TextLines[lineIndex];
- using (context.PushPostTransform(Matrix.CreateTranslation(offsetX, currentY)))
+ if (currentY + currentLine.Height > point.Y)
{
- textLine.Draw(context);
+ characterHit = currentLine.GetCharacterHitFromDistance(point.X);
+
+ return GetHitTestResult(currentLine, characterHit, point);
}
- currentY += textLine.LineMetrics.Size.Height;
+ currentY += currentLine.Height;
}
+
+ if (currentLine is null)
+ {
+ return new TextHitTestResult();
+ }
+
+ characterHit = currentLine.GetNextCaretCharacterHit(new CharacterHit(currentLine.TextRange.End));
+
+ return GetHitTestResult(currentLine, characterHit, point);
+ }
+
+ private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point)
+ {
+ var (x, y) = point;
+
+ var lastTrailingIndex = textLine.TextRange.Start + textLine.TextRange.Length;
+
+ var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height;
+
+ if (x >= textLine.Width && textLine.TextRange.Length > 0 && textLine.NewLineLength > 0)
+ {
+ lastTrailingIndex -= textLine.NewLineLength;
+ }
+
+ var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
+ y > Size.Height;
+
+ return new TextHitTestResult { IsInside = isInside, IsTrailing = isTrailing, TextPosition = textPosition };
}
///
@@ -155,7 +293,8 @@ namespace Avalonia.Media.TextFormatting
{
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
- return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, lineHeight);
+ return new GenericTextParagraphProperties(FlowDirection.LeftToRight, textAlignment, true, false,
+ textRunStyle, textWrapping, lineHeight, 0);
}
///
@@ -166,12 +305,14 @@ namespace Avalonia.Media.TextFormatting
/// The current height.
private static void UpdateBounds(TextLine textLine, ref double width, ref double height)
{
- if (width < textLine.LineMetrics.Size.Width)
+ var lineWidth = textLine.Width + textLine.Start * 2;
+
+ if (width < lineWidth)
{
- width = textLine.LineMetrics.Size.Width;
+ width = lineWidth;
}
- height += textLine.LineMetrics.Size.Height;
+ height += textLine.Height;
}
///
@@ -190,8 +331,9 @@ namespace Avalonia.Media.TextFormatting
new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties)
};
- return new TextLineImpl(textRuns,
- TextLineMetrics.Create(textRuns, new TextRange(startingIndex, 1), MaxWidth, _paragraphProperties));
+ var textRange = new TextRange(startingIndex, 1);
+
+ return new TextLineImpl(textRuns, textRange, MaxWidth, _paragraphProperties);
}
///
@@ -205,7 +347,7 @@ namespace Avalonia.Media.TextFormatting
TextLines = new List { textLine };
- Size = new Size(0, textLine.LineMetrics.Size.Height);
+ Size = new Size(0, textLine.Height);
}
else
{
@@ -230,7 +372,7 @@ namespace Avalonia.Media.TextFormatting
if (textLines.Count > 0)
{
if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) &&
- height + textLine.LineMetrics.Size.Height > MaxHeight)
+ height + textLine.Height > MaxHeight)
{
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{
@@ -244,7 +386,7 @@ namespace Avalonia.Media.TextFormatting
}
}
- var hasOverflowed = textLine.LineMetrics.HasOverflowed;
+ var hasOverflowed = textLine.HasOverflowed;
if (hasOverflowed && _textTrimming != TextTrimming.None)
{
@@ -257,7 +399,7 @@ namespace Avalonia.Media.TextFormatting
previousLine = textLine;
- if (currentPosition != _text.Length || textLine.TextLineBreak == null)
+ if (currentPosition != _text.Length || textLine.TextLineBreak?.RemainingCharacters == null)
{
continue;
}
@@ -290,6 +432,128 @@ namespace Avalonia.Media.TextFormatting
};
}
+ public int GetLineIndexFromCharacterIndex(int charIndex)
+ {
+ if (TextLines is null)
+ {
+ return -1;
+ }
+
+ if (charIndex < 0)
+ {
+ return -1;
+ }
+
+ if (charIndex > _text.Length - 1)
+ {
+ return TextLines.Count - 1;
+ }
+
+ for (var index = 0; index < TextLines.Count; index++)
+ {
+ var textLine = TextLines[index];
+
+ if (textLine.TextRange.End < charIndex)
+ {
+ continue;
+ }
+
+ if (charIndex >= textLine.Start && charIndex <= textLine.TextRange.End)
+ {
+ return index;
+ }
+ }
+
+ return TextLines.Count - 1;
+ }
+
+ public int GetCharacterIndexFromPoint(Point point, bool snapToText)
+ {
+ if (TextLines is null)
+ {
+ return -1;
+ }
+
+ var (x, y) = point;
+
+ if (!snapToText && y > Size.Height)
+ {
+ return -1;
+ }
+
+ var currentY = 0d;
+
+ foreach (var textLine in TextLines)
+ {
+ if (currentY + textLine.Height <= y)
+ {
+ currentY += textLine.Height;
+
+ continue;
+ }
+
+ if (x > textLine.WidthIncludingTrailingWhitespace)
+ {
+ if (snapToText)
+ {
+ return textLine.TextRange.End;
+ }
+
+ return -1;
+ }
+
+ var characterHit = textLine.GetCharacterHitFromDistance(x);
+
+ return characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+ }
+
+ return _text.Length;
+ }
+
+ public Rect GetRectFromCharacterIndex(int characterIndex, bool trailingEdge)
+ {
+ if (TextLines is null)
+ {
+ return Rect.Empty;
+ }
+
+ var distanceY = 0d;
+
+ var currentIndex = 0;
+
+ foreach (var textLine in TextLines)
+ {
+ if (currentIndex + textLine.TextRange.Length < characterIndex)
+ {
+ distanceY += textLine.Height;
+
+ currentIndex += textLine.TextRange.Length;
+
+ continue;
+ }
+
+ var characterHit = new CharacterHit(characterIndex);
+
+ while (characterHit.FirstCharacterIndex < characterIndex)
+ {
+ characterHit = textLine.GetNextCaretCharacterHit(characterHit);
+ }
+
+ var distanceX = textLine.GetDistanceFromCharacterHit(trailingEdge ?
+ characterHit :
+ new CharacterHit(characterHit.FirstCharacterIndex));
+
+ if (characterHit.TrailingLength > 0)
+ {
+ distanceX += 1;
+ }
+
+ return new Rect(distanceX, distanceY, 0, textLine.Height);
+ }
+
+ return Rect.Empty;
+ }
+
private readonly struct FormattedTextSource : ITextSource
{
private readonly ReadOnlySlice _text;
@@ -306,16 +570,16 @@ namespace Avalonia.Media.TextFormatting
public TextRun GetTextRun(int textSourceIndex)
{
- if (textSourceIndex > _text.End)
+ if (textSourceIndex > _text.Length)
{
- return new TextEndOfLine();
+ return null;
}
var runText = _text.Skip(textSourceIndex);
if (runText.IsEmpty)
{
- return new TextEndOfLine();
+ return new TextEndOfParagraph();
}
var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
index 8a1efa0611..e343279a43 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
@@ -7,6 +7,14 @@ namespace Avalonia.Media.TextFormatting
///
public abstract class TextLine
{
+ ///
+ /// Gets the text runs that are contained within a line.
+ ///
+ ///
+ /// The contained text runs.
+ ///
+ public abstract IReadOnlyList TextRuns { get; }
+
///
/// Gets the text range that is covered by the line.
///
@@ -16,28 +24,28 @@ namespace Avalonia.Media.TextFormatting
public abstract TextRange TextRange { get; }
///
- /// Gets the text runs.
+ /// Gets the state of the line when broken by line breaking process.
///
- ///
- /// The text runs.
- ///
- public abstract IReadOnlyList TextRuns { get; }
+ ///
+ /// A value that represents the line break.
+ ///
+ public abstract TextLineBreak TextLineBreak { get; }
///
- /// Gets the line metrics.
+ /// Gets the distance from the top to the baseline of the current TextLine object.
///
- ///
- /// The line metrics.
- ///
- public abstract TextLineMetrics LineMetrics { get; }
+ ///
+ /// A that represents the baseline distance.
+ ///
+ public abstract double Baseline { get; }
///
- /// Gets the state of the line when broken by line breaking process.
+ /// Gets the distance from the top-most to bottom-most black pixel in a line.
///
///
- /// A value that represents the line break.
+ /// A value that represents the extent distance.
///
- public abstract TextLineBreak TextLineBreak { get; }
+ public abstract double Extent { get; }
///
/// Gets a value that indicates whether the line is collapsed.
@@ -47,11 +55,92 @@ namespace Avalonia.Media.TextFormatting
///
public abstract bool HasCollapsed { get; }
+ ///
+ /// Gets a value that indicates whether content of the line overflows the specified paragraph width.
+ ///
+ ///
+ /// true, it the line overflows the specified paragraph width; otherwise, false.
+ ///
+ public abstract bool HasOverflowed { get; }
+
+ ///
+ /// Gets the height of a line of text.
+ ///
+ ///
+ /// The text line height.
+ ///
+ public abstract double Height { get; }
+
+ ///
+ /// Gets the number of newline characters at the end of a line.
+ ///
+ ///
+ /// The number of newline characters.
+ ///
+ public abstract int NewLineLength { get; }
+
+ ///
+ /// Gets the distance that black pixels extend beyond the bottom alignment edge of a line.
+ ///
+ ///
+ /// The overhang after distance.
+ ///
+ public abstract double OverhangAfter { get; }
+
+ ///
+ /// Gets the distance that black pixels extend prior to the left leading alignment edge of the line.
+ ///
+ ///
+ /// The overhang leading distance.
+ ///
+ public abstract double OverhangLeading { get; }
+
+ ///
+ /// Gets the distance that black pixels extend following the right trailing alignment edge of the line.
+ ///
+ ///
+ /// The overhang trailing distance.
+ ///
+ public abstract double OverhangTrailing { get; }
+
+ ///
+ /// Gets the distance from the start of a paragraph to the starting point of a line.
+ ///
+ ///
+ /// The distance from the start of a paragraph to the starting point of a line.
+ ///
+ public abstract double Start { get; }
+
+ ///
+ /// Gets the number of whitespace code points beyond the last non-blank character in a line.
+ ///
+ ///
+ /// The number of whitespace code points beyond the last non-blank character in a line.
+ ///
+ public abstract int TrailingWhitespaceLength { get; }
+
+ ///
+ /// Gets the width of a line of text, excluding trailing whitespace characters.
+ ///
+ ///
+ /// The text line width, excluding trailing whitespace characters.
+ ///
+ public abstract double Width { get; }
+
+ ///
+ /// Gets the width of a line of text, including trailing whitespace characters.
+ ///
+ ///
+ /// The text line width, including trailing whitespace characters.
+ ///
+ public abstract double WidthIncludingTrailingWhitespace { get; }
+
///
/// Draws the at the given origin.
///
/// The drawing context.
- public abstract void Draw(DrawingContext drawingContext);
+ ///
+ public abstract void Draw(DrawingContext drawingContext, Point lineOrigin);
///
/// Create a collapsed line based on collapsed text properties.
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
index c24454cb76..4ee0a9a28f 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
@@ -4,10 +4,20 @@ namespace Avalonia.Media.TextFormatting
{
public class TextLineBreak
{
+ public TextLineBreak(TextEndOfLine textEndOfLine)
+ {
+ TextEndOfLine = textEndOfLine;
+ }
+
public TextLineBreak(IReadOnlyList remainingCharacters)
{
RemainingCharacters = remainingCharacters;
}
+
+ ///
+ /// Get the
+ ///
+ public TextEndOfLine TextEndOfLine { get; }
///
/// Get the remaining shaped characters that were split up by the during the formatting process.
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
index fc98e9f6f8..b68e50c54e 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
@@ -1,30 +1,36 @@
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal class TextLineImpl : TextLine
{
- private readonly List _textRuns;
+ private readonly List _shapedTextRuns;
+ private readonly double _paragraphWidth;
+ private readonly TextParagraphProperties _paragraphProperties;
+ private readonly TextLineMetrics _textLineMetrics;
- public TextLineImpl(List textRuns, TextLineMetrics lineMetrics,
- TextLineBreak lineBreak = null, bool hasCollapsed = false)
+ public TextLineImpl(List textRuns, TextRange textRange, double paragraphWidth,
+ TextParagraphProperties paragraphProperties, TextLineBreak lineBreak = null, bool hasCollapsed = false)
{
- _textRuns = textRuns;
- LineMetrics = lineMetrics;
+ TextRange = textRange;
TextLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
- }
- ///
- public override TextRange TextRange => LineMetrics.TextRange;
+ _shapedTextRuns = textRuns;
+ _paragraphWidth = paragraphWidth;
+ _paragraphProperties = paragraphProperties;
+
+ _textLineMetrics = CreateLineMetrics();
+ }
///
- public override IReadOnlyList TextRuns => _textRuns;
+ public override IReadOnlyList TextRuns => _shapedTextRuns;
///
- public override TextLineMetrics LineMetrics { get; }
+ public override TextRange TextRange { get; }
///
public override TextLineBreak TextLineBreak { get; }
@@ -33,19 +39,52 @@ namespace Avalonia.Media.TextFormatting
public override bool HasCollapsed { get; }
///
- public override void Draw(DrawingContext drawingContext)
+ public override bool HasOverflowed => _textLineMetrics.HasOverflowed;
+
+ ///
+ public override double Baseline => _textLineMetrics.TextBaseline;
+
+ ///
+ public override double Extent => _textLineMetrics.Height;
+
+ ///
+ public override double Height => _textLineMetrics.Height;
+
+ ///
+ public override int NewLineLength => _textLineMetrics.NewLineLength;
+
+ ///
+ public override double OverhangAfter => 0;
+
+ ///
+ public override double OverhangLeading => 0;
+
+ ///
+ public override double OverhangTrailing => 0;
+
+ ///
+ public override int TrailingWhitespaceLength => _textLineMetrics.TrailingWhitespaceLength;
+
+ ///
+ public override double Start => _textLineMetrics.Start;
+
+ ///
+ public override double Width => _textLineMetrics.Width;
+
+ ///
+ public override double WidthIncludingTrailingWhitespace => _textLineMetrics.WidthIncludingTrailingWhitespace;
+
+ ///
+ public override void Draw(DrawingContext drawingContext, Point lineOrigin)
{
- var currentX = 0.0;
+ var (currentX, currentY) = lineOrigin;
- foreach (var textRun in _textRuns)
+ foreach (var textRun in _shapedTextRuns)
{
- var offsetY = LineMetrics.TextBaseline - textRun.GlyphRun.BaselineOrigin.Y;
-
- using (drawingContext.PushPostTransform(Matrix.CreateTranslation(currentX, offsetY)))
- {
- textRun.Draw(drawingContext);
- }
+ var offsetY = Baseline - textRun.GlyphRun.BaselineOrigin.Y;
+ textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
+
currentX += textRun.Size.Width;
}
}
@@ -59,19 +98,19 @@ namespace Avalonia.Media.TextFormatting
}
var collapsingProperties = collapsingPropertiesList[0];
+
var runIndex = 0;
var currentWidth = 0.0;
var textRange = TextRange;
var collapsedLength = 0;
- TextLineMetrics textLineMetrics;
var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol);
var availableWidth = collapsingProperties.Width - shapedSymbol.Size.Width;
- while (runIndex < _textRuns.Count)
+ while (runIndex < _shapedTextRuns.Count)
{
- var currentRun = _textRuns[runIndex];
+ var currentRun = _shapedTextRuns[runIndex];
currentWidth += currentRun.Size.Width;
@@ -87,14 +126,14 @@ namespace Avalonia.Media.TextFormatting
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
- var nextBreakPosition = lineBreaker.Current.PositionWrap;
+ var nextBreakPosition = lineBreaker.Current.PositionMeasure;
if (nextBreakPosition == 0)
{
break;
}
- if (nextBreakPosition > measuredLength)
+ if (nextBreakPosition >= measuredLength)
{
break;
}
@@ -108,7 +147,7 @@ namespace Avalonia.Media.TextFormatting
collapsedLength += measuredLength;
- var splitResult = TextFormatterImpl.SplitTextRuns(_textRuns, collapsedLength);
+ var splitResult = TextFormatterImpl.SplitTextRuns(_shapedTextRuns, collapsedLength);
var shapedTextCharacters = new List(splitResult.First.Count + 1);
@@ -118,12 +157,8 @@ namespace Avalonia.Media.TextFormatting
textRange = new TextRange(textRange.Start, collapsedLength);
- var shapedWidth = GetShapedWidth(shapedTextCharacters);
-
- textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height),
- LineMetrics.TextBaseline, textRange, false);
-
- return new TextLineImpl(shapedTextCharacters, textLineMetrics, TextLineBreak, true);
+ return new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties,
+ TextLineBreak, true);
}
availableWidth -= currentRun.Size.Width;
@@ -133,17 +168,71 @@ namespace Avalonia.Media.TextFormatting
runIndex++;
}
- textLineMetrics =
- new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Size.Width),
- LineMetrics.TextBaseline, TextRange, LineMetrics.HasOverflowed);
+ return this;
+ }
+
+ private TextLineMetrics CreateLineMetrics()
+ {
+ var width = 0d;
+ var widthIncludingWhitespace = 0d;
+ var trailingWhitespaceLength = 0;
+ var newLineLength = 0;
+ var ascent = 0d;
+ var descent = 0d;
+ var lineGap = 0d;
+
+ for (var index = 0; index < _shapedTextRuns.Count; index++)
+ {
+ var textRun = _shapedTextRuns[index];
+
+ var fontMetrics =
+ new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
+
+ if (ascent > fontMetrics.Ascent)
+ {
+ ascent = fontMetrics.Ascent;
+ }
+
+ if (descent < fontMetrics.Descent)
+ {
+ descent = fontMetrics.Descent;
+ }
+
+ if (lineGap < fontMetrics.LineGap)
+ {
+ lineGap = fontMetrics.LineGap;
+ }
+
+ if (index == _shapedTextRuns.Count - 1)
+ {
+ width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
+ widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
+ trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
+ newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
+ }
+ else
+ {
+ widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
+ }
+ }
+
+ var start = GetParagraphOffsetX(width, _paragraphWidth, _paragraphProperties.TextAlignment);
- return new TextLineImpl(new List(_textRuns) { shapedSymbol }, textLineMetrics, null,
- true);
+ var lineHeight = _paragraphProperties.LineHeight;
+
+ var height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
+ descent - ascent + lineGap :
+ lineHeight;
+
+ return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
+ -ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
}
///
public override CharacterHit GetCharacterHitFromDistance(double distance)
{
+ distance -= Start;
+
if (distance < 0)
{
// hit happens before the line, return the first position
@@ -153,7 +242,7 @@ namespace Avalonia.Media.TextFormatting
// process hit that happens within the line
var characterHit = new CharacterHit();
- foreach (var run in _textRuns)
+ foreach (var run in _shapedTextRuns)
{
characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _);
@@ -171,7 +260,32 @@ namespace Avalonia.Media.TextFormatting
///
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
- return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0));
+ var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0);
+
+ if (characterIndex > TextRange.End)
+ {
+ if (NewLineLength > 0)
+ {
+ return Start + Width;
+ }
+ return Start + WidthIncludingTrailingWhitespace;
+ }
+
+ var currentDistance = Start;
+
+ foreach (var textRun in _shapedTextRuns)
+ {
+ if (characterIndex > textRun.Text.End)
+ {
+ currentDistance += textRun.Size.Width;
+
+ continue;
+ }
+
+ return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(characterIndex));
+ }
+
+ return currentDistance;
}
///
@@ -189,7 +303,7 @@ namespace Avalonia.Media.TextFormatting
var runIndex = GetRunIndexAtCodepointIndex(TextRange.End);
- var textRun = _textRuns[runIndex];
+ var textRun = _shapedTextRuns[runIndex];
characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
@@ -219,28 +333,6 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit);
}
- ///
- /// Get distance from line start to the specified codepoint index.
- ///
- private double DistanceFromCodepointIndex(int codepointIndex)
- {
- var currentDistance = 0.0;
-
- foreach (var textRun in _textRuns)
- {
- if (codepointIndex > textRun.Text.End)
- {
- currentDistance += textRun.Size.Width;
-
- continue;
- }
-
- return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex));
- }
-
- return currentDistance;
- }
-
///
/// Tries to find the next character hit.
///
@@ -258,26 +350,28 @@ namespace Avalonia.Media.TextFormatting
return false; // Cannot go forward anymore
}
+ if (codepointIndex < TextRange.Start)
+ {
+ codepointIndex = TextRange.Start;
+ }
+
var runIndex = GetRunIndexAtCodepointIndex(codepointIndex);
- while (runIndex < TextRuns.Count)
+ while (runIndex < _shapedTextRuns.Count)
{
- var run = _textRuns[runIndex];
+ var run = _shapedTextRuns[runIndex];
- var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
+ var foundCharacterHit =
+ run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
TextRange.Length;
var characterIndex = codepointIndex - run.Text.Start;
- var codepoint = Codepoint.ReadAt(run.GlyphRun.Characters, characterIndex, out _);
-
- if (codepoint.IsBreakChar)
+ if (characterIndex < 0 && characterHit.TrailingLength == 0)
{
- foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(codepointIndex - 1, out _);
-
- isAtEnd = true;
+ foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
}
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
@@ -323,7 +417,7 @@ namespace Avalonia.Media.TextFormatting
while (runIndex >= 0)
{
- var run = _textRuns[runIndex];
+ var run = _shapedTextRuns[runIndex];
var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
@@ -349,9 +443,9 @@ namespace Avalonia.Media.TextFormatting
/// The text run index.
private int GetRunIndexAtCodepointIndex(int codepointIndex)
{
- if (codepointIndex >= TextRange.End)
+ if (codepointIndex > TextRange.End)
{
- return _textRuns.Count - 1;
+ return _shapedTextRuns.Count - 1;
}
if (codepointIndex <= 0)
@@ -361,11 +455,11 @@ namespace Avalonia.Media.TextFormatting
var runIndex = 0;
- while (runIndex < _textRuns.Count)
+ while (runIndex < _shapedTextRuns.Count)
{
- var run = _textRuns[runIndex];
+ var run = _shapedTextRuns[runIndex];
- if (run.Text.End > codepointIndex)
+ if (run.Text.End >= codepointIndex)
{
return runIndex;
}
@@ -392,24 +486,5 @@ namespace Avalonia.Media.TextFormatting
return new ShapedTextCharacters(glyphRun, textRun.Properties);
}
-
- ///
- /// Gets the shaped width of specified shaped text characters.
- ///
- /// The shaped text characters.
- ///
- /// The shaped width.
- ///
- private static double GetShapedWidth(IReadOnlyList shapedTextCharacters)
- {
- var shapedWidth = 0.0;
-
- for (var i = 0; i < shapedTextCharacters.Count; i++)
- {
- shapedWidth += shapedTextCharacters[i].Size.Width;
- }
-
- return shapedWidth;
- }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
index c4d7527659..1799c9d3db 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
@@ -1,7 +1,4 @@
-using System.Collections.Generic;
-using Avalonia.Utilities;
-
-namespace Avalonia.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
{
///
/// Represents a metric for a objects,
@@ -9,29 +6,39 @@ namespace Avalonia.Media.TextFormatting
///
public readonly struct TextLineMetrics
{
- public TextLineMetrics(Size size, double textBaseline, TextRange textRange, bool hasOverflowed)
+ public TextLineMetrics(bool hasOverflowed, double height, int newLineLength, double start, double textBaseline,
+ int trailingWhitespaceLength, double width,
+ double widthIncludingTrailingWhitespace)
{
- Size = size;
- TextBaseline = textBaseline;
- TextRange = textRange;
HasOverflowed = hasOverflowed;
+ Height = height;
+ NewLineLength = newLineLength;
+ Start = start;
+ TextBaseline = textBaseline;
+ TrailingWhitespaceLength = trailingWhitespaceLength;
+ Width = width;
+ WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
}
-
+
///
- /// Gets the text range that is covered by the text line.
+ /// Gets a value that indicates whether content of the line overflows the specified paragraph width.
///
- ///
- /// The text range that is covered by the text line.
- ///
- public TextRange TextRange { get; }
+ public bool HasOverflowed { get; }
///
- /// Gets the size of the text line.
+ /// Gets the height of a line of text.
///
- ///
- /// The size.
- ///
- public Size Size { get; }
+ public double Height { get; }
+
+ ///
+ /// Gets the number of newline characters at the end of a line.
+ ///
+ public int NewLineLength { get; }
+
+ ///
+ /// Gets the distance from the start of a paragraph to the starting point of a line.
+ ///
+ public double Start { get; }
///
/// Gets the distance from the top to the baseline of the line of text.
@@ -39,58 +46,18 @@ namespace Avalonia.Media.TextFormatting
public double TextBaseline { get; }
///
- /// Gets a boolean value that indicates whether content of the line overflows
- /// the specified paragraph width.
+ /// Gets the number of whitespace code points beyond the last non-blank character in a line.
///
- public bool HasOverflowed { get; }
+ public int TrailingWhitespaceLength { get; }
///
- /// Creates the text line metrics.
+ /// Gets the width of a line of text, excluding trailing whitespace characters.
///
- /// The text runs.
- /// The text range that is covered by the text line.
- /// The paragraph width.
- /// The text alignment.
- ///
- public static TextLineMetrics Create(IEnumerable textRuns, TextRange textRange, double paragraphWidth,
- TextParagraphProperties paragraphProperties)
- {
- var lineWidth = 0.0;
- var ascent = 0.0;
- var descent = 0.0;
- var lineGap = 0.0;
+ public double Width { get; }
- foreach (var textRun in textRuns)
- {
- var shapedRun = (ShapedTextCharacters)textRun;
-
- var fontMetrics =
- new FontMetrics(shapedRun.Properties.Typeface, shapedRun.Properties.FontRenderingEmSize);
-
- lineWidth += shapedRun.Size.Width;
-
- if (ascent > fontMetrics.Ascent)
- {
- ascent = fontMetrics.Ascent;
- }
-
- if (descent < fontMetrics.Descent)
- {
- descent = fontMetrics.Descent;
- }
-
- if (lineGap < fontMetrics.LineGap)
- {
- lineGap = fontMetrics.LineGap;
- }
- }
-
- var size = new Size(lineWidth,
- double.IsNaN(paragraphProperties.LineHeight) || MathUtilities.IsZero(paragraphProperties.LineHeight) ?
- descent - ascent + lineGap :
- paragraphProperties.LineHeight);
-
- return new TextLineMetrics(size, -ascent, textRange, size.Width > paragraphWidth);
- }
+ ///
+ /// Gets the width of a line of text, including trailing whitespace characters.
+ ///
+ public double WidthIncludingTrailingWhitespace { get; }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
index 3ecd1aafd9..4eff3fbd83 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
@@ -5,11 +5,36 @@
///
public abstract class TextParagraphProperties
{
+ ///
+ /// This property specifies whether the primary text advance
+ /// direction shall be left-to-right, right-to-left, or top-to-bottom.
+ ///
+ public abstract FlowDirection FlowDirection { get; }
+
///
/// Gets the text alignment.
///
public abstract TextAlignment TextAlignment { get; }
+ ///
+ /// Paragraph's line height
+ ///
+ public abstract double LineHeight { get; }
+
+ ///
+ /// Indicates the first line of the paragraph.
+ ///
+ public abstract bool FirstLineInParagraph { get; }
+
+ ///
+ /// If true, the formatted line may always be collapsed. If false (the default),
+ /// only lines that overflow the paragraph width are collapsed.
+ ///
+ public virtual bool AlwaysCollapsible
+ {
+ get { return false; }
+ }
+
///
/// Gets the default text style.
///
@@ -27,8 +52,16 @@
public abstract TextWrapping TextWrapping { get; }
///
- /// Paragraph's line height
+ /// Line indentation
///
- public abstract double LineHeight { get; }
+ public abstract double Indent { get; }
+
+ ///
+ /// Paragraph indentation
+ ///
+ public virtual double ParagraphIndent
+ {
+ get { return 0; }
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
index c15a771755..42cb5a7c46 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
@@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting
[DebuggerTypeProxy(typeof(TextRunDebuggerProxy))]
public abstract class TextRun
{
- public static readonly int DefaultTextSourceLength = 1;
+ public const int DefaultTextSourceLength = 1;
///
/// Gets the text source length.
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
index c4f9443c3d..468df33ab1 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
@@ -42,6 +42,11 @@ namespace Avalonia.Media.TextFormatting
///
public abstract CultureInfo CultureInfo { get; }
+ ///
+ /// Run vertical box alignment
+ ///
+ public abstract BaselineAlignment BaselineAlignment { get; }
+
public bool Equals(TextRunProperties other)
{
if (ReferenceEquals(null, other))
@@ -66,7 +71,7 @@ namespace Avalonia.Media.TextFormatting
{
unchecked
{
- var hashCode = (Typeface != null ? Typeface.GetHashCode() : 0);
+ var hashCode = Typeface.GetHashCode();
hashCode = (hashCode * 397) ^ FontRenderingEmSize.GetHashCode();
hashCode = (hashCode * 397) ^ (TextDecorations != null ? TextDecorations.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (ForegroundBrush != null ? ForegroundBrush.GetHashCode() : 0);
diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
index d6e88a7507..39d4066e55 100644
--- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
@@ -148,6 +148,17 @@ namespace Avalonia.Platform
/// Pops the latest pushed geometry clip.
///
void PopGeometryClip();
+
+ ///
+ /// Pushes a bitmap blending value.
+ ///
+ /// The bitmap blending mode.
+ void PushBitmapBlendMode(BitmapBlendingMode blendingMode);
+
+ ///
+ /// Pops the latest pushed bitmap blending value.
+ ///
+ void PopBitmapBlendMode();
///
/// Adds a custom draw operation
diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs
index 7490ad912a..79e125c5cb 100644
--- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs
@@ -11,6 +11,12 @@ namespace Avalonia.Platform
/// Gets the geometry's bounding rectangle.
///
Rect Bounds { get; }
+
+ ///
+ /// Gets the geometry's total length as if all its contours are placed
+ /// in a straight line.
+ ///
+ double ContourLength { get; }
///
/// Gets the geometry's bounding rectangle with the specified pen.
@@ -47,5 +53,38 @@ namespace Avalonia.Platform
/// The transform.
/// The cloned geometry.
ITransformedGeometryImpl WithTransform(Matrix transform);
+
+ ///
+ /// Attempts to get the corresponding point at the
+ /// specified distance
+ ///
+ /// The contour distance to get from.
+ /// The point in the specified distance.
+ /// If there's valid point at the specified distance.
+ bool TryGetPointAtDistance(double distance, out Point point);
+
+ ///
+ /// Attempts to get the corresponding point and
+ /// tangent from the specified distance along the
+ /// contour of the geometry.
+ ///
+ /// The contour distance to get from.
+ /// The point in the specified distance.
+ /// The tangent in the specified distance.
+ /// If there's valid point and tangent at the specified distance.
+ bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent);
+
+ ///
+ /// Attempts to get the corresponding path segment
+ /// given by the two distances specified.
+ /// Imagine it like snipping a part of the current
+ /// geometry.
+ ///
+ /// The contour distance to start snipping from.
+ /// The contour distance to stop snipping to.
+ /// If ture, the resulting snipped path will start with a BeginFigure call.
+ /// The resulting snipped path.
+ /// If the snipping operation is successful.
+ bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry);
}
}
diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
index fb4a4427b7..de67aca5a8 100644
--- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
@@ -100,6 +100,38 @@ namespace Avalonia.Platform
/// An .
IBitmapImpl LoadBitmap(Stream stream);
+ ///
+ /// Loads a WriteableBitmap implementation from a stream to a specified width maintaining aspect ratio.
+ ///
+ /// The stream to read the bitmap from.
+ /// The desired width of the resulting bitmap.
+ /// The to use should resizing be required.
+ /// An .
+ IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality);
+
+ ///
+ /// Loads a WriteableBitmap implementation from a stream to a specified height maintaining aspect ratio.
+ ///
+ /// The stream to read the bitmap from.
+ /// The desired height of the resulting bitmap.
+ /// The to use should resizing be required.
+ /// An .
+ IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality);
+
+ ///
+ /// Loads a WriteableBitmap implementation from a file.
+ ///
+ /// The filename of the bitmap.
+ /// An .
+ IWriteableBitmapImpl LoadWriteableBitmap(string fileName);
+
+ ///
+ /// Loads a WriteableBitmap implementation from a file.
+ ///
+ /// The stream to read the bitmap from.
+ /// An .
+ IWriteableBitmapImpl LoadWriteableBitmap(Stream stream);
+
///
/// Loads a bitmap implementation from a stream to a specified width maintaining aspect ratio.
///
@@ -136,9 +168,8 @@ namespace Avalonia.Platform
/// Creates a platform implementation of a glyph run.
///
/// The glyph run.
- /// The glyph run's width.
///
- IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width);
+ IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun);
bool SupportsIndividualRoundRects { get; }
diff --git a/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs b/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs
index 245888a351..354a344ffe 100644
--- a/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs
+++ b/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs
@@ -11,16 +11,13 @@ namespace Avalonia.Rendering
///
public interface ICustomSimpleHitTest
{
+ /// The point to hit test in global coordinate space.
bool HitTest(Point point);
}
///
/// Allows customization of hit-testing for all renderers.
///
- ///
- /// Note that this interface can only used to make a portion of a control non-hittable, it
- /// cannot expand the hittable area of a control.
- ///
public interface ICustomHitTest : ICustomSimpleHitTest
{
}
diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
index 9ea1b84311..85feb06c44 100644
--- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
@@ -225,7 +225,7 @@ namespace Avalonia.Rendering
if (filter?.Invoke(visual) != false)
{
- bool containsPoint = false;
+ bool containsPoint;
if (visual is ICustomSimpleHitTest custom)
{
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs
new file mode 100644
index 0000000000..0a5c1f8db6
--- /dev/null
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs
@@ -0,0 +1,68 @@
+using Avalonia.Platform;
+using Avalonia.Visuals.Media.Imaging;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+ ///
+ /// A node in the scene graph which represents an bitmap blending mode push or pop.
+ ///
+ internal class BitmapBlendModeNode : IDrawOperation
+ {
+ ///
+ /// Initializes a new instance of the class that represents an
+ /// push.
+ ///
+ /// The to push.
+ public BitmapBlendModeNode(BitmapBlendingMode bitmapBlend)
+ {
+ BlendingMode = bitmapBlend;
+ }
+
+ ///
+ /// Initializes a new instance of the class that represents an
+ /// pop.
+ ///
+ public BitmapBlendModeNode()
+ {
+ }
+
+ ///
+ public Rect Bounds => Rect.Empty;
+
+ ///
+ /// Gets the BitmapBlend to be pushed or null if the operation represents a pop.
+ ///
+ public BitmapBlendingMode? BlendingMode { get; }
+
+ ///
+ public bool HitTest(Point p) => false;
+
+ ///
+ /// Determines if this draw operation equals another.
+ ///
+ /// The opacity of the other draw operation.
+ /// True if the draw operations are the same, otherwise false.
+ ///
+ /// The properties of the other draw operation are passed in as arguments to prevent
+ /// allocation of a not-yet-constructed draw operation object.
+ ///
+ public bool Equals(BitmapBlendingMode? blendingMode) => BlendingMode == blendingMode;
+
+ ///
+ public void Render(IDrawingContextImpl context)
+ {
+ if (BlendingMode.HasValue)
+ {
+ context.PushBitmapBlendMode(BlendingMode.Value);
+ }
+ else
+ {
+ context.PopBitmapBlendMode();
+ }
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
index 28f426266d..3594cb59ee 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
@@ -253,6 +253,21 @@ namespace Avalonia.Rendering.SceneGraph
}
}
+ ///
+ public void PopBitmapBlendMode()
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(null))
+ {
+ Add(new BitmapBlendModeNode());
+ }
+ else
+ {
+ ++_drawOperationindex;
+ }
+ }
+
///
public void PopOpacity()
{
@@ -358,6 +373,21 @@ namespace Avalonia.Rendering.SceneGraph
}
}
+ ///
+ public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(blendingMode))
+ {
+ Add(new BitmapBlendModeNode(blendingMode));
+ }
+ else
+ {
+ ++_drawOperationindex;
+ }
+ }
+
public readonly struct UpdateState : IDisposable
{
public UpdateState(
@@ -378,9 +408,12 @@ namespace Avalonia.Rendering.SceneGraph
var dirty = Owner.Layers.GetOrAdd(Owner._node.LayerRoot).Dirty;
- foreach (var operation in Owner._node.DrawOperations)
+ var drawOperations = Owner._node.DrawOperations;
+ var drawOperationsCount = drawOperations.Count;
+
+ for (var i = 0; i < drawOperationsCount; i++)
{
- dirty.Add(operation.Item.Bounds);
+ dirty.Add(drawOperations[i].Item.Bounds);
}
Owner._node = Node;
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
index c9052c6ef2..d3da19d8c9 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
@@ -67,6 +67,14 @@ namespace Avalonia.Rendering.SceneGraph
/// The scaling mode.
///
public BitmapInterpolationMode BitmapInterpolationMode { get; }
+
+ ///
+ /// The bitmap blending mode.
+ ///
+ ///
+ /// The blending mode.
+ ///
+ public BitmapBlendingMode BitmapBlendingMode { get; }
///
/// Determines if this draw operation equals another.
@@ -85,12 +93,12 @@ namespace Avalonia.Rendering.SceneGraph
public bool Equals(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
{
return transform == Transform &&
- Equals(source.Item, Source.Item) &&
- source.Item.Version == SourceVersion &&
- opacity == Opacity &&
- sourceRect == SourceRect &&
- destRect == DestRect &&
- bitmapInterpolationMode == BitmapInterpolationMode;
+ Equals(source.Item, Source.Item) &&
+ source.Item.Version == SourceVersion &&
+ opacity == Opacity &&
+ sourceRect == SourceRect &&
+ destRect == DestRect &&
+ bitmapInterpolationMode == BitmapInterpolationMode;
}
///
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
index d0c4566485..187c1da0a9 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
@@ -84,7 +84,7 @@ namespace Avalonia.Rendering.SceneGraph
return transform == Transform &&
Equals(brush, Brush) &&
Equals(Pen, pen) &&
- Media.BoxShadows.Equals(BoxShadows, boxShadows) &&
+ BoxShadows.Equals(boxShadows) &&
rect.Equals(Rect);
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
index 6a4c532d4a..36aa08c2f9 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
@@ -256,7 +256,8 @@ namespace Avalonia.Rendering.SceneGraph
if (childCount == 0 || wasVisited)
{
- if ((wasVisited || FilterAndClip(node, ref clip)) && node.HitTest(_point))
+ if ((wasVisited || FilterAndClip(node, ref clip)) &&
+ (node.Visual is ICustomSimpleHitTest custom ? custom.HitTest(_point) : node.HitTest(_point)))
{
_current = node.Visual;
@@ -311,8 +312,7 @@ namespace Avalonia.Rendering.SceneGraph
if (!clipped && node.Visual is ICustomHitTest custom)
{
- var controlPoint = _sceneRoot.Visual.TranslatePoint(_point, node.Visual);
- clipped = !custom.HitTest(controlPoint.Value);
+ clipped = !custom.HitTest(_point);
}
return !clipped;
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
index 7d5d62a091..56f05db04e 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
@@ -1,5 +1,5 @@
using System;
-using System.Linq;
+using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
@@ -247,11 +247,26 @@ namespace Avalonia.Rendering.SceneGraph
if (forceRecurse)
{
- foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance))
+ var visualChildren = (IList) visual.VisualChildren;
+
+ if (visualChildren.Count == 1)
{
- var childNode = GetOrCreateChildNode(scene, child, node);
+ var childNode = GetOrCreateChildNode(scene, visualChildren[0], node);
Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
}
+ else if (visualChildren.Count > 1)
+ {
+ var sortedChildren = new IVisual[visualChildren.Count];
+ visualChildren.CopyTo(sortedChildren, 0);
+
+ Array.Sort(sortedChildren, ZIndexComparer.ComparisonInstance);
+
+ foreach (var child in sortedChildren)
+ {
+ var childNode = GetOrCreateChildNode(scene, child, node);
+ Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
+ }
+ }
node.SubTreeUpdated = true;
contextImpl.TrimChildren();
@@ -308,13 +323,17 @@ namespace Avalonia.Rendering.SceneGraph
private static void Deindex(Scene scene, VisualNode node)
{
- foreach (VisualNode child in node.Children)
+ var nodeChildren = node.Children;
+ var nodeChildrenCount = nodeChildren.Count;
+
+ for (var i = 0; i < nodeChildrenCount; i++)
{
- if (child is VisualNode visual)
+ if (nodeChildren[i] is VisualNode visual)
{
Deindex(scene, visual);
}
}
+
scene.Remove(node);
node.SubTreeUpdated = true;
@@ -323,7 +342,6 @@ namespace Avalonia.Rendering.SceneGraph
node.Visual.TransformedBounds = null;
-
if (node.LayerRoot == node.Visual && node.Visual != scene.Root.Visual)
{
scene.Layers.Remove(node.LayerRoot);
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
index 8fb6b2542a..d0439feed2 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
@@ -365,9 +365,12 @@ namespace Avalonia.Rendering.SceneGraph
{
var result = new Rect();
- foreach (var operation in DrawOperations)
+ if (_drawOperations != null)
{
- result = result.Union(operation.Item.Bounds);
+ foreach (var operation in _drawOperations)
+ {
+ result = result.Union(operation.Item.Bounds);
+ }
}
_bounds = result;
diff --git a/src/Avalonia.Visuals/Rendering/ZIndexComparer.cs b/src/Avalonia.Visuals/Rendering/ZIndexComparer.cs
index 3ed076f22e..e9700c70a5 100644
--- a/src/Avalonia.Visuals/Rendering/ZIndexComparer.cs
+++ b/src/Avalonia.Visuals/Rendering/ZIndexComparer.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
@@ -6,6 +7,7 @@ namespace Avalonia.Rendering
public class ZIndexComparer : IComparer
{
public static readonly ZIndexComparer Instance = new ZIndexComparer();
+ public static readonly Comparison ComparisonInstance = Instance.Compare;
public int Compare(IVisual x, IVisual y) => (x?.ZIndex ?? 0).CompareTo(y?.ZIndex ?? 0);
}
diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs
index 1b9f5c67d5..79c4202be4 100644
--- a/src/Avalonia.Visuals/Vector.cs
+++ b/src/Avalonia.Visuals/Vector.cs
@@ -82,6 +82,15 @@ namespace Avalonia
public static Vector operator *(Vector vector, double scale)
=> Multiply(vector, scale);
+ ///
+ /// Scales a vector.
+ ///
+ /// The vector.
+ /// The scaling factor.
+ /// The scaled vector.
+ public static Vector operator *(double scale, Vector vector)
+ => Multiply(vector, scale);
+
///
/// Scales a vector.
///
diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs
index 80d4195421..2c96d87bb6 100644
--- a/src/Avalonia.Visuals/Visual.cs
+++ b/src/Avalonia.Visuals/Visual.cs
@@ -11,6 +11,8 @@ using Avalonia.Rendering;
using Avalonia.Utilities;
using Avalonia.VisualTree;
+#nullable enable
+
namespace Avalonia
{
///
@@ -45,8 +47,8 @@ namespace Avalonia
///
/// Defines the property.
///
- public static readonly StyledProperty ClipProperty =
- AvaloniaProperty.Register(nameof(Clip));
+ public static readonly StyledProperty ClipProperty =
+ AvaloniaProperty.Register(nameof(Clip));
///
/// Defines the property.
@@ -63,14 +65,14 @@ namespace Avalonia
///
/// Defines the property.
///
- public static readonly StyledProperty OpacityMaskProperty =
- AvaloniaProperty.Register(nameof(OpacityMask));
+ public static readonly StyledProperty OpacityMaskProperty =
+ AvaloniaProperty.Register(nameof(OpacityMask));
///
/// Defines the property.
///
- public static readonly StyledProperty RenderTransformProperty =
- AvaloniaProperty.Register(nameof(RenderTransform));
+ public static readonly StyledProperty RenderTransformProperty =
+ AvaloniaProperty.Register(nameof(RenderTransform));
///
/// Defines the property.
@@ -81,8 +83,8 @@ namespace Avalonia
///
/// Defines the property.
///
- public static readonly DirectProperty VisualParentProperty =
- AvaloniaProperty.RegisterDirect("VisualParent", o => o._visualParent);
+ public static readonly DirectProperty VisualParentProperty =
+ AvaloniaProperty.RegisterDirect(nameof(IVisual.VisualParent), o => o._visualParent);
///
/// Defines the property.
@@ -92,8 +94,8 @@ namespace Avalonia
private Rect _bounds;
private TransformedBounds? _transformedBounds;
- private IRenderRoot _visualRoot;
- private IVisual _visualParent;
+ private IRenderRoot? _visualRoot;
+ private IVisual? _visualParent;
///
/// Initializes static members of the class.
@@ -128,12 +130,12 @@ namespace Avalonia
///
/// Raised when the control is attached to a rooted visual tree.
///
- public event EventHandler AttachedToVisualTree;
+ public event EventHandler? AttachedToVisualTree;
///
/// Raised when the control is detached from a rooted visual tree.
///
- public event EventHandler DetachedFromVisualTree;
+ public event EventHandler? DetachedFromVisualTree;
///
/// Gets the bounds of the control relative to its parent.
@@ -161,7 +163,7 @@ namespace Avalonia
///
/// Gets or sets the geometry clip for this visual.
///
- public Geometry Clip
+ public Geometry? Clip
{
get { return GetValue(ClipProperty); }
set { SetValue(ClipProperty, value); }
@@ -174,7 +176,7 @@ namespace Avalonia
{
get
{
- IVisual node = this;
+ IVisual? node = this;
while (node != null)
{
@@ -211,7 +213,7 @@ namespace Avalonia
///
/// Gets or sets the opacity mask of the control.
///
- public IBrush OpacityMask
+ public IBrush? OpacityMask
{
get { return GetValue(OpacityMaskProperty); }
set { SetValue(OpacityMaskProperty, value); }
@@ -220,7 +222,7 @@ namespace Avalonia
///
/// Gets or sets the render transform of the control.
///
- public ITransform RenderTransform
+ public ITransform? RenderTransform
{
get { return GetValue(RenderTransformProperty); }
set { SetValue(RenderTransformProperty, value); }
@@ -261,7 +263,7 @@ namespace Avalonia
///
/// Gets the root of the visual tree, if the control is attached to a visual tree.
///
- protected IRenderRoot VisualRoot => _visualRoot ?? (this as IRenderRoot);
+ protected IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot);
///
/// Gets a value indicating whether this control is attached to a visual root.
@@ -276,12 +278,12 @@ namespace Avalonia
///
/// Gets the control's parent visual.
///
- IVisual IVisual.VisualParent => _visualParent;
+ IVisual? IVisual.VisualParent => _visualParent;
///
/// Gets the root of the visual tree, if the control is attached to a visual tree.
///
- IRenderRoot IVisual.VisualRoot => VisualRoot;
+ IRenderRoot? IVisual.VisualRoot => VisualRoot;
TransformedBounds? IVisual.TransformedBounds
{
@@ -476,12 +478,12 @@ namespace Avalonia
///
/// The old visual parent.
/// The new visual parent.
- protected virtual void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
+ protected virtual void OnVisualParentChanged(IVisual? oldParent, IVisual? newParent)
{
RaisePropertyChanged(
VisualParentProperty,
- new Optional(oldParent),
- new BindingValue(newParent),
+ new Optional(oldParent),
+ new BindingValue(newParent),
BindingPriority.LocalValue);
}
@@ -582,7 +584,7 @@ namespace Avalonia
/// Sets the visual parent of the Visual.
///
/// The visual parent.
- private void SetVisualParent(Visual value)
+ private void SetVisualParent(Visual? value)
{
if (_visualParent == value)
{
@@ -634,7 +636,7 @@ namespace Avalonia
}
}
- private static void SetVisualParent(IList children, Visual parent)
+ private static void SetVisualParent(IList children, Visual? parent)
{
var count = children.Count;
diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs
index 50787655d9..97c4554de6 100644
--- a/src/Avalonia.Visuals/VisualTree/IVisual.cs
+++ b/src/Avalonia.Visuals/VisualTree/IVisual.cs
@@ -3,6 +3,8 @@ using Avalonia.Collections;
using Avalonia.Media;
using Avalonia.Rendering;
+#nullable enable
+
namespace Avalonia.VisualTree
{
///
@@ -21,12 +23,12 @@ namespace Avalonia.VisualTree
///
/// Raised when the control is attached to a rooted visual tree.
///
- event EventHandler AttachedToVisualTree;
+ event EventHandler? AttachedToVisualTree;
///
/// Raised when the control is detached from a rooted visual tree.
///
- event EventHandler DetachedFromVisualTree;
+ event EventHandler? DetachedFromVisualTree;
///
/// Gets the bounds of the control relative to its parent.
@@ -46,7 +48,7 @@ namespace Avalonia.VisualTree
///
/// Gets or sets the geometry clip for this visual.
///
- Geometry Clip { get; set; }
+ Geometry? Clip { get; set; }
///
/// Gets a value indicating whether this control is attached to a visual root.
@@ -71,12 +73,12 @@ namespace Avalonia.VisualTree
///
/// Gets or sets the opacity mask for the control.
///
- IBrush OpacityMask { get; set; }
+ IBrush? OpacityMask { get; set; }
///
/// Gets or sets the render transform of the control.
///
- ITransform RenderTransform { get; set; }
+ ITransform? RenderTransform { get; set; }
///
/// Gets or sets the render transform origin of the control.
@@ -91,12 +93,12 @@ namespace Avalonia.VisualTree
///
/// Gets the control's parent visual.
///
- IVisual VisualParent { get; }
+ IVisual? VisualParent { get; }
///
/// Gets the root of the visual tree, if the control is attached to a visual tree.
///
- IRenderRoot VisualRoot { get; }
+ IRenderRoot? VisualRoot { get; }
///
/// Gets or sets the Z index of the node.
diff --git a/src/Avalonia.X11/X11Window.Ime.cs b/src/Avalonia.X11/X11Window.Ime.cs
index ac626f5825..0bd1bc8a89 100644
--- a/src/Avalonia.X11/X11Window.Ime.cs
+++ b/src/Avalonia.X11/X11Window.Ime.cs
@@ -96,14 +96,14 @@ namespace Avalonia.X11
void HandleKeyEvent(ref XEvent ev)
{
- var index = ev.KeyEvent.state.HasFlagCustom(XModifierMask.ShiftMask);
+ var index = ev.KeyEvent.state.HasAllFlags(XModifierMask.ShiftMask);
// We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32();
// Manually switch the Shift index for the keypad,
// there should be a proper way to do this
- if (ev.KeyEvent.state.HasFlagCustom(XModifierMask.Mod2Mask)
+ if (ev.KeyEvent.state.HasAllFlags(XModifierMask.Mod2Mask)
&& key > X11Key.Num_Lock && key <= X11Key.KP_9)
key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32();
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 3650bf1c79..00ed01a461 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -641,23 +641,23 @@ namespace Avalonia.X11
RawInputModifiers TranslateModifiers(XModifierMask state)
{
var rv = default(RawInputModifiers);
- if (state.HasFlagCustom(XModifierMask.Button1Mask))
+ if (state.HasAllFlags(XModifierMask.Button1Mask))
rv |= RawInputModifiers.LeftMouseButton;
- if (state.HasFlagCustom(XModifierMask.Button2Mask))
+ if (state.HasAllFlags(XModifierMask.Button2Mask))
rv |= RawInputModifiers.RightMouseButton;
- if (state.HasFlagCustom(XModifierMask.Button3Mask))
+ if (state.HasAllFlags(XModifierMask.Button3Mask))
rv |= RawInputModifiers.MiddleMouseButton;
- if (state.HasFlagCustom(XModifierMask.Button4Mask))
+ if (state.HasAllFlags(XModifierMask.Button4Mask))
rv |= RawInputModifiers.XButton1MouseButton;
- if (state.HasFlagCustom(XModifierMask.Button5Mask))
+ if (state.HasAllFlags(XModifierMask.Button5Mask))
rv |= RawInputModifiers.XButton2MouseButton;
- if (state.HasFlagCustom(XModifierMask.ShiftMask))
+ if (state.HasAllFlags(XModifierMask.ShiftMask))
rv |= RawInputModifiers.Shift;
- if (state.HasFlagCustom(XModifierMask.ControlMask))
+ if (state.HasAllFlags(XModifierMask.ControlMask))
rv |= RawInputModifiers.Control;
- if (state.HasFlagCustom(XModifierMask.Mod1Mask))
+ if (state.HasAllFlags(XModifierMask.Mod1Mask))
rv |= RawInputModifiers.Alt;
- if (state.HasFlagCustom(XModifierMask.Mod4Mask))
+ if (state.HasAllFlags(XModifierMask.Mod4Mask))
rv |= RawInputModifiers.Meta;
return rv;
}
diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs
index 2874c517a9..7bf1df41b6 100644
--- a/src/Avalonia.X11/XI2Manager.cs
+++ b/src/Avalonia.X11/XI2Manager.cs
@@ -342,13 +342,13 @@ namespace Avalonia.X11
Type = ev->evtype;
Timestamp = (ulong)ev->time.ToInt64();
var state = (XModifierMask)ev->mods.Effective;
- if (state.HasFlagCustom(XModifierMask.ShiftMask))
+ if (state.HasAllFlags(XModifierMask.ShiftMask))
Modifiers |= RawInputModifiers.Shift;
- if (state.HasFlagCustom(XModifierMask.ControlMask))
+ if (state.HasAllFlags(XModifierMask.ControlMask))
Modifiers |= RawInputModifiers.Control;
- if (state.HasFlagCustom(XModifierMask.Mod1Mask))
+ if (state.HasAllFlags(XModifierMask.Mod1Mask))
Modifiers |= RawInputModifiers.Alt;
- if (state.HasFlagCustom(XModifierMask.Mod4Mask))
+ if (state.HasAllFlags(XModifierMask.Mod4Mask))
Modifiers |= RawInputModifiers.Meta;
Modifiers |= ParseButtonState(ev->buttons.MaskLen, ev->buttons.Mask);
@@ -364,7 +364,7 @@ namespace Avalonia.X11
if (Type == XiEventType.XI_ButtonPress || Type == XiEventType.XI_ButtonRelease)
Button = ev->detail;
Detail = ev->detail;
- Emulated = ev->flags.HasFlagCustom(XiDeviceEventFlags.XIPointerEmulated);
+ Emulated = ev->flags.HasAllFlags(XiDeviceEventFlags.XIPointerEmulated);
}
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs
new file mode 100644
index 0000000000..b3fc979fca
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Threading;
+using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods;
+
+namespace Avalonia.LinuxFramebuffer.Input.EvDev
+{
+ public class EvDevBackend : IInputBackend
+ {
+ private readonly EvDevDeviceDescription[] _deviceDescriptions;
+ private readonly List _handlers = new List();
+ private int _epoll;
+ private Queue _inputQueue = new Queue();
+ private bool _isQueueHandlerTriggered;
+ private object _lock = new object();
+ private Action _onInput;
+ private IInputRoot _inputRoot;
+
+ public EvDevBackend(EvDevDeviceDescription[] devices)
+ {
+ _deviceDescriptions = devices;
+ }
+
+ unsafe void InputThread()
+ {
+ const int MaxEvents = 16;
+ var events = stackalloc epoll_event[MaxEvents];
+ while (true)
+ {
+ var eventCount = Math.Min(MaxEvents, epoll_wait(_epoll, events, MaxEvents, 1000));
+ for (var c = 0; c < eventCount; c++)
+ {
+ try
+ {
+ var ev = events[c];
+ var handler = _handlers[(int)ev.data.u32];
+ handler.HandleEvents();
+ }
+ catch (Exception e)
+ {
+ Console.Error.WriteLine(e.ToString());
+ }
+ }
+ }
+ }
+
+ private void OnRawEvent(RawInputEventArgs obj)
+ {
+ lock (_lock)
+ {
+ _inputQueue.Enqueue(obj);
+ TriggerQueueHandler();
+ }
+
+ }
+
+ void TriggerQueueHandler()
+ {
+ if (_isQueueHandlerTriggered)
+ return;
+ _isQueueHandlerTriggered = true;
+ Dispatcher.UIThread.Post(InputQueueHandler, DispatcherPriority.Input);
+
+ }
+
+ void InputQueueHandler()
+ {
+ RawInputEventArgs ev;
+ lock (_lock)
+ {
+ _isQueueHandlerTriggered = false;
+ if(_inputQueue.Count == 0)
+ return;
+ ev = _inputQueue.Dequeue();
+ }
+
+ _onInput?.Invoke(ev);
+
+ lock (_lock)
+ {
+ if (_inputQueue.Count > 0)
+ TriggerQueueHandler();
+ }
+ }
+
+ public void Initialize(IScreenInfoProvider info, Action onInput)
+ {
+ _onInput = onInput;
+ _epoll = epoll_create1(0);
+ for (var c = 0; c < _deviceDescriptions.Length; c++)
+ {
+ var description = _deviceDescriptions[c];
+ var dev = EvDevDevice.Open(description.Path);
+ EvDevDeviceHandler handler;
+ if (description is EvDevTouchScreenDeviceDescription touch)
+ handler = new EvDevSingleTouchScreen(dev, touch, info) { InputRoot = _inputRoot };
+ else
+ throw new Exception("Unknown device description type " + description.GetType().FullName);
+
+ handler.OnEvent += OnRawEvent;
+ _handlers.Add(handler);
+
+ var ev = new epoll_event { events = EPOLLIN, data = { u32 = (uint)c } };
+ epoll_ctl(_epoll, EPOLL_CTL_ADD, dev.Fd, ref ev);
+ }
+
+ new Thread(InputThread) { IsBackground = true }.Start();
+ }
+
+ public void SetInputRoot(IInputRoot root)
+ {
+ _inputRoot = root;
+ foreach (var h in _handlers)
+ h.InputRoot = root;
+ }
+
+
+ public static EvDevBackend CreateFromEnvironment()
+ {
+ var env = Environment.GetEnvironmentVariables();
+ var deviceDescriptions = new List();
+ foreach (string key in env.Keys)
+ {
+ if (key.StartsWith("AVALONIA_EVDEV_DEVICE_"))
+ {
+ var value = (string)env[key];
+ deviceDescriptions.Add(EvDevDeviceDescription.ParseFromEnv(value));
+ }
+ }
+
+ if (deviceDescriptions.Count == 0)
+ throw new Exception(
+ "No device device description found, specify devices by adding AVALONIA_EVDEV_DEVICE_{name} environment variables");
+
+ return new EvDevBackend(deviceDescriptions.ToArray());
+ }
+ }
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs
new file mode 100644
index 0000000000..e7978a6e23
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.LinuxFramebuffer.Input.EvDev
+{
+ unsafe class EvDevDevice
+ {
+ public int Fd { get; }
+ private IntPtr _dev;
+ public string Name { get; }
+ public List EventTypes { get; private set; } = new List();
+ public input_absinfo? AbsX { get; }
+ public input_absinfo? AbsY { get; }
+
+ public EvDevDevice(int fd, IntPtr dev)
+ {
+ Fd = fd;
+ _dev = dev;
+ Name = Marshal.PtrToStringAnsi(NativeUnsafeMethods.libevdev_get_name(_dev));
+ foreach (EvType type in Enum.GetValues(typeof(EvType)))
+ {
+ if (NativeUnsafeMethods.libevdev_has_event_type(dev, type) != 0)
+ EventTypes.Add(type);
+ }
+ var ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int) AbsAxis.ABS_X);
+ if (ptr != null)
+ AbsX = *ptr;
+ ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int)AbsAxis.ABS_Y);
+ if (ptr != null)
+ AbsY = *ptr;
+ }
+
+ public input_event? NextEvent()
+ {
+ input_event ev;
+ if (NativeUnsafeMethods.libevdev_next_event(_dev, 2, out ev) == 0)
+ return ev;
+ return null;
+ }
+
+ public static EvDevDevice Open(string device)
+ {
+ var fd = NativeUnsafeMethods.open(device, 2048, 0);
+ if (fd <= 0)
+ throw new Exception($"Unable to open {device} code {Marshal.GetLastWin32Error()}");
+ IntPtr dev;
+ var rc = NativeUnsafeMethods.libevdev_new_from_fd(fd, out dev);
+ if (rc < 0)
+ {
+ NativeUnsafeMethods.close(fd);
+ throw new Exception($"Unable to initialize evdev for {device} code {Marshal.GetLastWin32Error()}");
+ }
+ return new EvDevDevice(fd, dev);
+ }
+ }
+
+ internal class EvDevAxisInfo
+ {
+ public int Minimum { get; set; }
+ public int Maximum { get; set; }
+ }
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDeviceDescription.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDeviceDescription.cs
new file mode 100644
index 0000000000..659748bfbb
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDeviceDescription.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Linq;
+
+namespace Avalonia.LinuxFramebuffer.Input.EvDev
+{
+ public abstract class EvDevDeviceDescription
+ {
+ protected internal EvDevDeviceDescription()
+ {
+
+ }
+
+ public string Path { get; set; }
+
+ internal static EvDevDeviceDescription ParseFromEnv(string env)
+ {
+ var formatEx = new ArgumentException(
+ "Invalid device format, expected `(path):type=(touchscreen):[calibration=m11,m12,m21,m22,m31,m32]");
+
+
+ var items = env.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
+ if (items.Length < 2)
+ throw formatEx;
+ var path = items[0];
+ var dic = items.Skip(1)
+ .Select(i => i.Split(new[] { '=' }, 2))
+ .ToDictionary(x => x[0], x => x[1]);
+
+ if (!dic.TryGetValue("type", out var type))
+ throw formatEx;
+
+ if (type == "touchscreen")
+ return EvDevTouchScreenDeviceDescription.ParseFromEnv(path, dic);
+
+ throw formatEx;
+ }
+ }
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs
new file mode 100644
index 0000000000..b69b151c3b
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs
@@ -0,0 +1,114 @@
+using System;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+
+namespace Avalonia.LinuxFramebuffer.Input.EvDev
+{
+ internal class EvDevSingleTouchScreen : EvDevDeviceHandler
+ {
+ private readonly IScreenInfoProvider _screenInfo;
+ private readonly int _width, _height;
+ private readonly Matrix _calibration;
+ private input_absinfo _axisX;
+ private input_absinfo _axisY;
+ private TouchDevice _device = new TouchDevice();
+
+ private int _currentX, _currentY;
+ private bool _hasMovement;
+ private bool? _pressAction;
+
+ public EvDevSingleTouchScreen(EvDevDevice device, EvDevTouchScreenDeviceDescription description,
+ IScreenInfoProvider screenInfo) : base(device)
+ {
+ if (device.AbsX == null || device.AbsY == null)
+ throw new ArgumentException("Device is not a touchscreen");
+ _screenInfo = screenInfo;
+
+ _calibration = description.CalibrationMatrix;
+ _axisX = device.AbsX.Value;
+ _axisY = device.AbsY.Value;
+ }
+
+ protected override void HandleEvent(input_event ev)
+ {
+ if (ev.Type == EvType.EV_ABS)
+ {
+ if (ev.Axis == AbsAxis.ABS_X)
+ {
+ _currentX = ev.value;
+ _hasMovement = true;
+ }
+
+ if (ev.Axis == AbsAxis.ABS_Y)
+ {
+ _currentY = ev.value;
+ _hasMovement = true;
+ }
+ }
+
+ if (ev.Type == EvType.EV_KEY)
+ {
+ if (ev.Key == EvKey.BTN_TOUCH)
+ {
+ _pressAction = ev.value != 0;
+ }
+ }
+
+ if (ev.Type == EvType.EV_SYN)
+ {
+ if (_pressAction != null)
+ RaiseEvent(_pressAction == true ? RawPointerEventType.TouchBegin : RawPointerEventType.TouchEnd,
+ ev.Timestamp);
+ else if(_hasMovement)
+ RaiseEvent(RawPointerEventType.TouchUpdate, ev.Timestamp);
+ _hasMovement = false;
+ _pressAction = null;
+ }
+ }
+
+ void RaiseEvent(RawPointerEventType type, ulong timestamp)
+ {
+ var point = new Point(_currentX, _currentY);
+
+ var touchWidth = _axisX.maximum - _axisX.minimum;
+ var touchHeight = _axisY.maximum - _axisY.minimum;
+
+ var screenSize = _screenInfo.ScaledSize;
+
+ // Normalize to 0-(max-min)
+ point -= new Point(_axisX.minimum, _axisY.minimum);
+
+ // Apply calibration matrix
+ point *= _calibration;
+
+ // Transform to display pixel grid
+ point = new Point(point.X * screenSize.Width / touchWidth, point.Y * screenSize.Height / touchHeight);
+
+ RaiseEvent(new RawTouchEventArgs(_device, timestamp, InputRoot,
+ type, point, RawInputModifiers.None, 1));
+ }
+ }
+
+ internal abstract class EvDevDeviceHandler
+ {
+ public event Action OnEvent;
+ public EvDevDeviceHandler(EvDevDevice device)
+ {
+ Device = device;
+ }
+
+ public EvDevDevice Device { get; }
+ public IInputRoot InputRoot { get; set; }
+
+ public void HandleEvents()
+ {
+ input_event? ev;
+ while ((ev = Device.NextEvent()) != null)
+ HandleEvent(ev.Value);
+ }
+
+ protected void RaiseEvent(RawInputEventArgs ev) => OnEvent?.Invoke(ev);
+
+ protected abstract void HandleEvent(input_event ev);
+ }
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreenDeviceDescription.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreenDeviceDescription.cs
new file mode 100644
index 0000000000..9d006cfc31
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreenDeviceDescription.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace Avalonia.LinuxFramebuffer.Input.EvDev
+{
+ public sealed class EvDevTouchScreenDeviceDescription : EvDevDeviceDescription
+ {
+ public Matrix CalibrationMatrix { get; set; } = Matrix.Identity;
+
+ internal static EvDevTouchScreenDeviceDescription ParseFromEnv(string path, Dictionary options)
+ {
+ var calibrationMatrix = Matrix.Identity;
+ if (options.TryGetValue("calibration", out var calibration))
+ calibrationMatrix = Matrix.Parse(calibration);
+
+ return new EvDevTouchScreenDeviceDescription { Path = path, CalibrationMatrix = calibrationMatrix };
+ }
+ }
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
index 3c8d3b5e06..a6b70069c1 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
@@ -8,12 +8,15 @@ using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.LinuxFramebuffer;
+using Avalonia.LinuxFramebuffer.Input;
+using Avalonia.LinuxFramebuffer.Input.EvDev;
using Avalonia.LinuxFramebuffer.Input.LibInput;
using Avalonia.LinuxFramebuffer.Output;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
+using JetBrains.Annotations;
namespace Avalonia.LinuxFramebuffer
{
@@ -58,6 +61,7 @@ namespace Avalonia.LinuxFramebuffer
class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime
{
private readonly IOutputBackend _fb;
+ [CanBeNull] private readonly IInputBackend _inputBackend;
private TopLevel _topLevel;
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
public CancellationToken Token => _cts.Token;
@@ -67,6 +71,12 @@ namespace Avalonia.LinuxFramebuffer
_fb = fb;
}
+ public LinuxFramebufferLifetime(IOutputBackend fb, IInputBackend input)
+ {
+ _fb = fb;
+ _inputBackend = input;
+ }
+
public Control MainView
{
get => (Control)_topLevel?.Content;
@@ -74,8 +84,16 @@ namespace Avalonia.LinuxFramebuffer
{
if (_topLevel == null)
{
+ var inputBackend = _inputBackend;
+ if (inputBackend == null)
+ {
+ if (Environment.GetEnvironmentVariable("AVALONIA_USE_EVDEV") == "1")
+ inputBackend = EvDevBackend.CreateFromEnvironment();
+ else
+ inputBackend = new LibInputBackend();
+ }
- var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, new LibInputBackend()));
+ var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, inputBackend));
tl.Prepare();
_topLevel = tl;
_topLevel.Renderer.Start();
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs
index 18db176bcd..f71d4e9753 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs
@@ -50,6 +50,19 @@ namespace Avalonia.LinuxFramebuffer
public static extern IntPtr libevdev_get_name(IntPtr dev);
[DllImport("libevdev.so.2", EntryPoint = "libevdev_get_abs_info", SetLastError = true)]
public static extern input_absinfo* libevdev_get_abs_info(IntPtr dev, int code);
+
+ [DllImport("libc")]
+ public extern static int epoll_create1(int size);
+
+ [DllImport("libc")]
+ public extern static int epoll_ctl(int epfd, int op, int fd, ref epoll_event __event);
+
+ [DllImport("libc")]
+ public extern static int epoll_wait(int epfd, epoll_event* events, int maxevents, int timeout);
+
+ public const int EPOLLIN = 1;
+ public const int EPOLL_CTL_ADD = 1;
+ public const int O_NONBLOCK = 2048;
}
[StructLayout(LayoutKind.Sequential)]
@@ -190,9 +203,22 @@ namespace Avalonia.LinuxFramebuffer
[StructLayout(LayoutKind.Sequential)]
struct input_event
{
- private IntPtr crap1, crap2;
- public ushort type, code;
+ private IntPtr timeval1, timeval2;
+ public ushort _type, _code;
public int value;
+ public EvType Type => (EvType)_type;
+ public EvKey Key => (EvKey)_code;
+ public AbsAxis Axis => (AbsAxis)_code;
+
+ public ulong Timestamp
+ {
+ get
+ {
+ var ms = (ulong)timeval2.ToInt64() / 1000;
+ var s = (ulong)timeval1.ToInt64() * 1000;
+ return s + ms;
+ }
+ }
}
[StructLayout(LayoutKind.Sequential)]
@@ -249,7 +275,8 @@ namespace Avalonia.LinuxFramebuffer
{
BTN_LEFT = 0x110,
BTN_RIGHT = 0x111,
- BTN_MIDDLE = 0x112
+ BTN_MIDDLE = 0x112,
+ BTN_TOUCH = 0x14a
}
[StructLayout(LayoutKind.Sequential)]
@@ -263,4 +290,24 @@ namespace Avalonia.LinuxFramebuffer
public __s32 resolution;
}
+
+ [StructLayout(LayoutKind.Explicit)]
+ struct epoll_data
+ {
+ [FieldOffset(0)]
+ public IntPtr ptr;
+ [FieldOffset(0)]
+ public int fd;
+ [FieldOffset(0)]
+ public uint u32;
+ [FieldOffset(0)]
+ public ulong u64;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ struct epoll_event
+ {
+ public uint events;
+ public epoll_data data;
+ }
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
index 34cc261187..e218da5f41 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
@@ -54,7 +54,7 @@ namespace Avalonia.LinuxFramebuffer.Output
}
public PixelSize Resolution => new PixelSize(Mode.hdisplay, Mode.vdisplay);
- public bool IsPreferred => Mode.type.HasFlagCustom(DrmModeType.DRM_MODE_TYPE_PREFERRED);
+ public bool IsPreferred => Mode.type.HasAllFlags(DrmModeType.DRM_MODE_TYPE_PREFERRED);
public string Name { get; }
}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index abff763bb1..a191dc59fb 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
@@ -41,10 +41,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
// Targeted
InsertBefore(
+ new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
new AvaloniaXamlIlTransformInstanceAttachedProperties(),
new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers());
InsertAfter(
- new AvaloniaXamlIlAvaloniaPropertyResolver());
+ new AvaloniaXamlIlAvaloniaPropertyResolver(),
+ new AvaloniaXamlIlReorderClassesPropertiesTransformer()
+ );
InsertBefore(
new AvaloniaXamlIlBindingPathParser(),
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs
index 0c0dcb1634..f6f47dce0d 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs
@@ -14,8 +14,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
XamlXmlnsMappings xmlnsMappings,
XamlValueConverter customValueConverter,
XamlIlClrPropertyInfoEmitter clrPropertyEmitter,
- XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter)
- : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter)
+ XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter,
+ IXamlIdentifierGenerator identifierGenerator = null)
+ : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter, identifierGenerator)
{
ClrPropertyEmitter = clrPropertyEmitter;
AccessorFactoryEmitter = accessorFactoryEmitter;
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs
new file mode 100644
index 0000000000..23232dbcf3
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs
@@ -0,0 +1,97 @@
+using System.Collections.Generic;
+using XamlX.Ast;
+using XamlX.Emit;
+using XamlX.IL;
+using XamlX.Transform;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
+{
+ class AvaloniaXamlIlResolveClassesPropertiesTransformer : IXamlAstTransformer
+ {
+ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+ {
+ if (node is XamlAstNamePropertyReference prop
+ && prop.TargetType is XamlAstClrTypeReference targetRef
+ && prop.DeclaringType is XamlAstClrTypeReference declaringRef)
+ {
+ var types = context.GetAvaloniaTypes();
+ if (types.StyledElement.IsAssignableFrom(targetRef.Type)
+ && types.Classes.Equals(declaringRef.Type))
+ {
+ return new XamlAstClrProperty(node, "class:" + prop.Name, types.Classes,
+ null)
+ {
+ Setters = { new ClassValueSetter(types, prop.Name), new ClassBindingSetter(types, prop.Name) }
+ };
+ }
+ }
+ return node;
+ }
+
+
+ class ClassValueSetter : IXamlEmitablePropertySetter
+ {
+ private readonly AvaloniaXamlIlWellKnownTypes _types;
+ private readonly string _className;
+
+ public ClassValueSetter(AvaloniaXamlIlWellKnownTypes types, string className)
+ {
+ _types = types;
+ _className = className;
+ Parameters = new[] { types.XamlIlTypes.Boolean };
+ }
+
+ public void Emit(IXamlILEmitter emitter)
+ {
+ using (var value = emitter.LocalsPool.GetLocal(_types.XamlIlTypes.Boolean))
+ {
+ emitter
+ .Stloc(value.Local)
+ .EmitCall(_types.StyledElementClassesProperty.Getter)
+ .Ldstr(_className)
+ .Ldloc(value.Local)
+ .EmitCall(_types.Classes.GetMethod(new FindMethodMethodSignature("Set",
+ _types.XamlIlTypes.Void, _types.XamlIlTypes.String, _types.XamlIlTypes.Boolean)));
+ }
+ }
+
+ public IXamlType TargetType => _types.StyledElement;
+
+ public PropertySetterBinderParameters BinderParameters { get; } =
+ new PropertySetterBinderParameters { AllowXNull = false };
+ public IReadOnlyList Parameters { get; }
+ }
+
+ class ClassBindingSetter : IXamlEmitablePropertySetter
+ {
+ private readonly AvaloniaXamlIlWellKnownTypes _types;
+ private readonly string _className;
+
+ public ClassBindingSetter(AvaloniaXamlIlWellKnownTypes types, string className)
+ {
+ _types = types;
+ _className = className;
+ Parameters = new[] {types.IBinding};
+ }
+
+ public void Emit(IXamlILEmitter emitter)
+ {
+ using (var bloc = emitter.LocalsPool.GetLocal(_types.IBinding))
+ emitter
+ .Stloc(bloc.Local)
+ .Ldstr(_className)
+ .Ldloc(bloc.Local)
+ // TODO: provide anchor?
+ .Ldnull();
+ emitter.EmitCall(_types.ClassesBindMethod, true);
+ }
+
+ public IXamlType TargetType => _types.StyledElement;
+
+ public PropertySetterBinderParameters BinderParameters { get; } =
+ new PropertySetterBinderParameters { AllowXNull = false };
+ public IReadOnlyList Parameters { get; }
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs
new file mode 100644
index 0000000000..ae3515a6d6
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs
@@ -0,0 +1,40 @@
+using XamlX.Ast;
+using XamlX.Transform;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
+{
+ class AvaloniaXamlIlReorderClassesPropertiesTransformer : IXamlAstTransformer
+ {
+ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+ {
+ if (node is XamlAstObjectNode obj)
+ {
+ IXamlAstNode classesNode = null;
+ IXamlAstNode firstSingleClassNode = null;
+ var types = context.GetAvaloniaTypes();
+ foreach (var child in obj.Children)
+ {
+ if (child is XamlAstXamlPropertyValueNode propValue
+ && propValue.Property is XamlAstClrProperty prop)
+ {
+ if (prop.DeclaringType.Equals(types.Classes))
+ {
+ if (firstSingleClassNode == null)
+ firstSingleClassNode = child;
+ }
+ else if (prop.Name == "Classes" && prop.DeclaringType.Equals(types.StyledElement))
+ classesNode = child;
+ }
+ }
+
+ if (classesNode != null && firstSingleClassNode != null)
+ {
+ obj.Children.Remove(classesNode);
+ obj.Children.Insert(obj.Children.IndexOf(firstSingleClassNode), classesNode);
+ }
+ }
+
+ return node;
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
index 34aae2c5ed..c4995b2de3 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
@@ -25,6 +25,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType AssignBindingAttribute { get; }
public IXamlType UnsetValueType { get; }
public IXamlType StyledElement { get; }
+ public IXamlType IStyledElement { get; }
public IXamlType NameScope { get; }
public IXamlMethod NameScopeSetNameScope { get; }
public IXamlType INameScope { get; }
@@ -78,6 +79,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType ColumnDefinition { get; }
public IXamlType ColumnDefinitions { get; }
public IXamlType Classes { get; }
+ public IXamlMethod ClassesBindMethod { get; }
+ public IXamlProperty StyledElementClassesProperty { get; set; }
public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{
@@ -97,6 +100,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
IBinding, cfg.WellKnownTypes.Object);
UnsetValueType = cfg.TypeSystem.GetType("Avalonia.UnsetValueType");
StyledElement = cfg.TypeSystem.GetType("Avalonia.StyledElement");
+ IStyledElement = cfg.TypeSystem.GetType("Avalonia.IStyledElement");
INameScope = cfg.TypeSystem.GetType("Avalonia.Controls.INameScope");
INameScopeRegister = INameScope.GetMethod(
new FindMethodMethodSignature("Register", XamlIlTypes.Void,
@@ -168,6 +172,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
RowDefinition = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinition");
RowDefinitions = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinitions");
Classes = cfg.TypeSystem.GetType("Avalonia.Controls.Classes");
+ StyledElementClassesProperty =
+ StyledElement.Properties.First(x => x.Name == "Classes" && x.PropertyType.Equals(Classes));
+ ClassesBindMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions")
+ .FindMethod( "BindClass", IDisposable, false, IStyledElement,
+ cfg.WellKnownTypes.String,
+ IBinding, cfg.WellKnownTypes.Object);
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
index 07c5451135..650534b347 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
@@ -30,7 +30,7 @@ namespace Avalonia.Markup.Xaml.Templates
public IControl Build(object data, IControl existing)
{
- return existing ?? TemplateContent.Load(Content).Control;
+ return existing ?? TemplateContent.Load(Content)?.Control;
}
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
index c8843a3176..c096ed7ed7 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
@@ -10,8 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates
[TemplateContent]
public object Content { get; set; }
- public IPanel Build()
- => (IPanel)TemplateContent.Load(Content).Control;
+ public IPanel Build() => (IPanel)TemplateContent.Load(Content)?.Control;
object ITemplate.Build() => Build();
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
index 65323ae665..45fae9cb28 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
@@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates
[TemplateContent]
public object Content { get; set; }
- public IControl Build() => TemplateContent.Load(Content).Control;
+ public IControl Build() => TemplateContent.Load(Content)?.Control;
object ITemplate.Build() => Build();
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
index 96f25668fb..483a1a5d06 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
@@ -1,6 +1,4 @@
using System;
-using Avalonia.Controls;
-using System.Collections.Generic;
using Avalonia.Controls.Templates;
namespace Avalonia.Markup.Xaml.Templates
@@ -14,6 +12,12 @@ namespace Avalonia.Markup.Xaml.Templates
{
return (ControlTemplateResult)direct(null);
}
+
+ if (templateContent is null)
+ {
+ return null;
+ }
+
throw new ArgumentException(nameof(templateContent));
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
index d785ac4ac0..7b065c7f47 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
@@ -51,8 +51,12 @@ namespace Avalonia.Markup.Xaml.Templates
public IControl Build(object data)
{
- var visualTreeForItem = TemplateContent.Load(Content).Control;
- visualTreeForItem.DataContext = data;
+ var visualTreeForItem = TemplateContent.Load(Content)?.Control;
+ if (visualTreeForItem != null)
+ {
+ visualTreeForItem.DataContext = data;
+ }
+
return visualTreeForItem;
}
}
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index 2a79a4bb50..2c5c2acd52 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -23,9 +23,11 @@ namespace Avalonia.Skia
private readonly Vector _dpi;
private readonly Stack _maskStack = new Stack();
private readonly Stack _opacityStack = new Stack();
+ private readonly Stack _blendingModeStack = new Stack();
private readonly Matrix? _postTransform;
private readonly IVisualBrushRenderer _visualBrushRenderer;
private double _currentOpacity = 1.0f;
+ private BitmapBlendingMode _currentBlendingMode = BitmapBlendingMode.SourceOver;
private readonly bool _canTextUseLcdRendering;
private Matrix _currentTransform;
private bool _disposed;
@@ -145,6 +147,7 @@ namespace Avalonia.Skia
})
{
paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality();
+ paint.BlendMode = _currentBlendingMode.ToSKBlendMode();
drawableImage.Draw(this, s, d, paint);
}
@@ -163,7 +166,10 @@ namespace Avalonia.Skia
{
using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
{
- Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint);
+ if (paint.Paint is object)
+ {
+ Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, paint.Paint);
+ }
}
}
@@ -358,7 +364,6 @@ namespace Avalonia.Skia
{
Canvas.DrawRect(rc, paint.Paint);
}
-
}
}
@@ -394,15 +399,17 @@ namespace Avalonia.Skia
{
using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size))
{
- if (isRounded)
+ if (paint.Paint is object)
{
- Canvas.DrawRoundRect(skRoundRect, paint.Paint);
- }
- else
- {
- Canvas.DrawRect(rc, paint.Paint);
+ if (isRounded)
+ {
+ Canvas.DrawRoundRect(skRoundRect, paint.Paint);
+ }
+ else
+ {
+ Canvas.DrawRect(rc, paint.Paint);
+ }
}
-
}
}
}
@@ -508,6 +515,19 @@ namespace Avalonia.Skia
Canvas.Restore();
}
+ ///
+ public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
+ {
+ _blendingModeStack.Push(_currentBlendingMode);
+ _currentBlendingMode = blendingMode;
+ }
+
+ ///
+ public void PopBitmapBlendMode()
+ {
+ _currentBlendingMode = _blendingModeStack.Pop();
+ }
+
public void Custom(ICustomDrawOperation custom) => custom.Render(this);
///
diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs
index 879b18742e..19dbcf9713 100644
--- a/src/Skia/Avalonia.Skia/GeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs
@@ -11,9 +11,36 @@ namespace Avalonia.Skia
internal abstract class GeometryImpl : IGeometryImpl
{
private PathCache _pathCache;
-
+ private SKPathMeasure _pathMeasureCache;
+
+ private SKPathMeasure CachedPathMeasure
+ {
+ get
+ {
+ if (_pathMeasureCache is null)
+ {
+ _pathMeasureCache = new SKPathMeasure(EffectivePath);
+ }
+
+ return _pathMeasureCache;
+ }
+ }
+
///
public abstract Rect Bounds { get; }
+
+ ///
+ public double ContourLength
+ {
+ get
+ {
+ if (EffectivePath is null)
+ return 0;
+
+ return (double)CachedPathMeasure?.Length;
+ }
+ }
+
public abstract SKPath EffectivePath { get; }
///
@@ -30,12 +57,12 @@ namespace Avalonia.Skia
// Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic.
var strokeWidth = (float)(pen?.Thickness ?? 0);
-
+
if (!_pathCache.HasCacheFor(strokeWidth))
{
UpdatePathCache(strokeWidth);
}
-
+
return PathContainsCore(_pathCache.CachedStrokePath, point);
}
@@ -58,7 +85,7 @@ namespace Avalonia.Skia
{
paint.IsStroke = true;
paint.StrokeWidth = strokeWidth;
-
+
paint.GetFillPath(EffectivePath, strokePath);
_pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect());
@@ -74,13 +101,13 @@ namespace Avalonia.Skia
/// True, if point is contained in a path.
private static bool PathContainsCore(SKPath path, Point point)
{
- return path.Contains((float)point.X, (float)point.Y);
+ return path.Contains((float)point.X, (float)point.Y);
}
///
public IGeometryImpl Intersect(IGeometryImpl geometry)
{
- var result = EffectivePath.Op(((GeometryImpl) geometry).EffectivePath, SKPathOp.Intersect);
+ var result = EffectivePath.Op(((GeometryImpl)geometry).EffectivePath, SKPathOp.Intersect);
return result == null ? null : new StreamGeometryImpl(result);
}
@@ -89,21 +116,74 @@ namespace Avalonia.Skia
public Rect GetRenderBounds(IPen pen)
{
var strokeWidth = (float)(pen?.Thickness ?? 0);
-
+
if (!_pathCache.HasCacheFor(strokeWidth))
{
UpdatePathCache(strokeWidth);
}
-
+
return _pathCache.CachedGeometryRenderBounds;
}
-
+
///
public ITransformedGeometryImpl WithTransform(Matrix transform)
{
return new TransformedGeometryImpl(this, transform);
}
+ ///
+ public bool TryGetPointAtDistance(double distance, out Point point)
+ {
+ if (EffectivePath is null)
+ {
+ point = new Point();
+ return false;
+ }
+
+ var res = CachedPathMeasure.GetPosition((float)distance, out var skPoint);
+ point = new Point(skPoint.X, skPoint.Y);
+ return res;
+ }
+
+ ///
+ public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent)
+ {
+ if (EffectivePath is null)
+ {
+ point = new Point();
+ tangent = new Point();
+ return false;
+ }
+
+ var res = CachedPathMeasure.GetPositionAndTangent((float)distance, out var skPoint, out var skTangent);
+ point = new Point(skPoint.X, skPoint.Y);
+ tangent = new Point(skTangent.X, skTangent.Y);
+ return res;
+ }
+
+ public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure,
+ out IGeometryImpl segmentGeometry)
+ {
+ if (EffectivePath is null)
+ {
+ segmentGeometry = null;
+ return false;
+ }
+
+ segmentGeometry = null;
+
+ var _skPathSegment = new SKPath();
+
+ var res = CachedPathMeasure.GetSegment((float)startDistance, (float)stopDistance, _skPathSegment, startOnBeginFigure);
+
+ if (res)
+ {
+ segmentGeometry = new StreamGeometryImpl(_skPathSegment);
+ }
+
+ return res;
+ }
+
///
/// Invalidate all caches. Call after chaining path contents.
///
@@ -115,12 +195,12 @@ namespace Avalonia.Skia
private struct PathCache
{
private float _cachedStrokeWidth;
-
+
///
/// Tolerance for two stroke widths to be deemed equal
///
public const float Tolerance = float.Epsilon;
-
+
///
/// Cached contour path.
///
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index 6d0be9f64d..7bc83ec85b 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -77,6 +77,31 @@ namespace Avalonia.Skia
return new ImmutableBitmap(stream);
}
+ public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ return new WriteableBitmapImpl(stream, width, true, interpolationMode);
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ return new WriteableBitmapImpl(stream, height, false, interpolationMode);
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(string fileName)
+ {
+ using (var stream = File.OpenRead(fileName))
+ {
+ return LoadWriteableBitmap(stream);
+ }
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream)
+ {
+ return new WriteableBitmapImpl(stream);
+ }
+
///
public IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride)
{
@@ -171,7 +196,7 @@ namespace Avalonia.Skia
private static readonly ThreadLocal s_textBlobBuilderThreadLocal = new ThreadLocal(() => new SKTextBlobBuilder());
///
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
var count = glyphRun.GlyphIndices.Length;
var textBlobBuilder = s_textBlobBuilderThreadLocal.Value;
@@ -183,11 +208,8 @@ namespace Avalonia.Skia
s_font.Size = (float)glyphRun.FontRenderingEmSize;
s_font.Typeface = typeface;
-
SKTextBlob textBlob;
- width = 0;
-
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
if (glyphRun.GlyphOffsets.IsEmpty)
@@ -197,8 +219,6 @@ namespace Avalonia.Skia
textBlobBuilder.AddRun(glyphRun.GlyphIndices.Buffer.Span, s_font);
textBlob = textBlobBuilder.Build();
-
- width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
else
{
@@ -206,6 +226,8 @@ namespace Avalonia.Skia
var positions = buffer.GetPositionSpan();
+ var width = 0d;
+
for (var i = 0; i < count; i++)
{
positions[i] = (float)width;
@@ -251,13 +273,10 @@ namespace Avalonia.Skia
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
- width = currentX;
-
textBlob = textBlobBuilder.Build();
}
return new GlyphRunImpl(textBlob);
-
}
public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi)
diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
index bb3dbbfadc..75b4231640 100644
--- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
+++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
@@ -25,6 +25,39 @@ namespace Avalonia.Skia
}
}
+ public static SKBlendMode ToSKBlendMode(this BitmapBlendingMode blendingMode)
+ {
+ switch (blendingMode)
+ {
+ case BitmapBlendingMode.SourceOver:
+ return SKBlendMode.SrcOver;
+ case BitmapBlendingMode.Source:
+ return SKBlendMode.Src;
+ case BitmapBlendingMode.SourceIn:
+ return SKBlendMode.SrcIn;
+ case BitmapBlendingMode.SourceOut:
+ return SKBlendMode.SrcOut;
+ case BitmapBlendingMode.SourceAtop:
+ return SKBlendMode.SrcATop;
+ case BitmapBlendingMode.Destination:
+ return SKBlendMode.Dst;
+ case BitmapBlendingMode.DestinationIn:
+ return SKBlendMode.DstIn;
+ case BitmapBlendingMode.DestinationOut:
+ return SKBlendMode.DstOut;
+ case BitmapBlendingMode.DestinationOver:
+ return SKBlendMode.DstOver;
+ case BitmapBlendingMode.DestinationAtop:
+ return SKBlendMode.DstATop;
+ case BitmapBlendingMode.Xor:
+ return SKBlendMode.Xor;
+ case BitmapBlendingMode.Plus:
+ return SKBlendMode.Plus;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(blendingMode), blendingMode, null);
+ }
+ }
+
public static SKPoint ToSKPoint(this Point p)
{
return new SKPoint((float)p.X, (float)p.Y);
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index 31724bfee9..5cf72e2ce8 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
@@ -87,7 +87,7 @@ namespace Avalonia.Skia
{
var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _);
- if (nextCodepoint == '\r' && codepoint == '\n' || nextCodepoint == '\n' && codepoint == '\r')
+ if (nextCodepoint == '\n' && codepoint == '\r')
{
count++;
diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
index d48e7d10e6..63a1e8f869 100644
--- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
+++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
@@ -1,8 +1,10 @@
using System;
using System.IO;
using System.Threading;
+using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Skia.Helpers;
+using Avalonia.Visuals.Media.Imaging;
using SkiaSharp;
namespace Avalonia.Skia
@@ -15,7 +17,70 @@ namespace Avalonia.Skia
private static readonly SKBitmapReleaseDelegate s_releaseDelegate = ReleaseProc;
private readonly SKBitmap _bitmap;
private readonly object _lock = new object();
+
+ ///
+ /// Create a WriteableBitmap from given stream.
+ ///
+ /// Stream containing encoded data.
+ public WriteableBitmapImpl(Stream stream)
+ {
+ using (var skiaStream = new SKManagedStream(stream))
+ {
+ _bitmap = SKBitmap.Decode(skiaStream);
+
+ if (_bitmap == null)
+ {
+ throw new ArgumentException("Unable to load bitmap from provided data");
+ }
+
+ PixelSize = new PixelSize(_bitmap.Width, _bitmap.Height);
+ Dpi = SkiaPlatform.DefaultDpi;
+ }
+ }
+
+ public WriteableBitmapImpl(Stream stream, int decodeSize, bool horizontal, BitmapInterpolationMode interpolationMode)
+ {
+ using (var skStream = new SKManagedStream(stream))
+ using (var codec = SKCodec.Create(skStream))
+ {
+ var info = codec.Info;
+ // get the scale that is nearest to what we want (eg: jpg returned 512)
+ var supportedScale = codec.GetScaledDimensions(horizontal ? ((float)decodeSize / info.Width) : ((float)decodeSize / info.Height));
+
+ // decode the bitmap at the nearest size
+ var nearest = new SKImageInfo(supportedScale.Width, supportedScale.Height);
+ var bmp = SKBitmap.Decode(codec, nearest);
+
+ // now scale that to the size that we want
+ var realScale = horizontal ? ((double)info.Height / info.Width) : ((double)info.Width / info.Height);
+
+ SKImageInfo desired;
+
+
+ if (horizontal)
+ {
+ desired = new SKImageInfo(decodeSize, (int)(realScale * decodeSize));
+ }
+ else
+ {
+ desired = new SKImageInfo((int)(realScale * decodeSize), decodeSize);
+ }
+
+ if (bmp.Width != desired.Width || bmp.Height != desired.Height)
+ {
+ var scaledBmp = bmp.Resize(desired, interpolationMode.ToSKFilterQuality());
+ bmp.Dispose();
+ bmp = scaledBmp;
+ }
+
+ _bitmap = bmp;
+
+ PixelSize = new PixelSize(bmp.Width, bmp.Height);
+ Dpi = SkiaPlatform.DefaultDpi;
+ }
+ }
+
///
/// Create new writeable bitmap.
///
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index 6ae27870e8..f50167b39a 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -188,6 +188,28 @@ namespace Avalonia.Direct2D1
return new WicBitmapImpl(stream);
}
+ public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ return new WriteableWicBitmapImpl(stream, width, true, interpolationMode);
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ return new WriteableWicBitmapImpl(stream, height, false, interpolationMode);
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(string fileName)
+ {
+ return new WriteableWicBitmapImpl(fileName);
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream)
+ {
+ return new WriteableWicBitmapImpl(stream);
+ }
+
///
public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
@@ -213,7 +235,7 @@ namespace Avalonia.Direct2D1
return new WicBitmapImpl(format, alphaFormat, data, size, dpi, stride);
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
@@ -236,8 +258,6 @@ namespace Avalonia.Direct2D1
run.Advances = new float[glyphCount];
- width = 0;
-
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
if (glyphRun.GlyphAdvances.IsEmpty)
@@ -247,8 +267,6 @@ namespace Avalonia.Direct2D1
var advance = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
run.Advances[i] = advance;
-
- width += advance;
}
}
else
@@ -258,8 +276,6 @@ namespace Avalonia.Direct2D1
var advance = (float)glyphRun.GlyphAdvances[i];
run.Advances[i] = advance;
-
- width += advance;
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
index 47a19aad8c..af35934785 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
@@ -5,6 +5,7 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
+using Avalonia.Visuals.Media.Imaging;
using SharpDX;
using SharpDX.Direct2D1;
using SharpDX.Mathematics.Interop;
@@ -121,7 +122,9 @@ namespace Avalonia.Direct2D1.Media
using (var d2d = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext))
{
var interpolationMode = GetInterpolationMode(bitmapInterpolationMode);
-
+
+ // TODO: How to implement CompositeMode here?
+
_deviceContext.DrawBitmap(
d2d.Value,
destRect.ToSharpDX(),
@@ -149,6 +152,35 @@ namespace Avalonia.Direct2D1.Media
}
}
+ public static CompositeMode GetCompositeMode(BitmapBlendingMode blendingMode)
+ {
+ switch (blendingMode)
+ {
+ case BitmapBlendingMode.SourceIn:
+ return CompositeMode.SourceIn;
+ case BitmapBlendingMode.SourceOut:
+ return CompositeMode.SourceOut;
+ case BitmapBlendingMode.SourceOver:
+ return CompositeMode.SourceOver;
+ case BitmapBlendingMode.SourceAtop:
+ return CompositeMode.SourceAtop;
+ case BitmapBlendingMode.DestinationIn:
+ return CompositeMode.DestinationIn;
+ case BitmapBlendingMode.DestinationOut:
+ return CompositeMode.DestinationOut;
+ case BitmapBlendingMode.DestinationOver:
+ return CompositeMode.DestinationOver;
+ case BitmapBlendingMode.DestinationAtop:
+ return CompositeMode.DestinationAtop;
+ case BitmapBlendingMode.Xor:
+ return CompositeMode.Xor;
+ case BitmapBlendingMode.Plus:
+ return CompositeMode.Plus;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(blendingMode), blendingMode, null);
+ }
+ }
+
///
/// Draws a bitmap image.
///
@@ -525,6 +557,16 @@ namespace Avalonia.Direct2D1.Media
PopLayer();
}
+ public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
+ {
+ // TODO: Stubs for now
+ }
+
+ public void PopBitmapBlendMode()
+ {
+ // TODO: Stubs for now
+ }
+
public void PushOpacityMask(IBrush mask, Rect bounds)
{
var parameters = new LayerParameters
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
index d04e2b3110..ec88347a17 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
@@ -1,3 +1,4 @@
+using Avalonia.Logging;
using Avalonia.Platform;
using SharpDX.Direct2D1;
@@ -8,6 +9,8 @@ namespace Avalonia.Direct2D1.Media
///
public abstract class GeometryImpl : IGeometryImpl
{
+ private const float ContourApproximation = 0.0001f;
+
public GeometryImpl(Geometry geometry)
{
Geometry = geometry;
@@ -16,6 +19,9 @@ namespace Avalonia.Direct2D1.Media
///
public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia();
+ ///
+ public double ContourLength => Geometry.ComputeLength(null, ContourApproximation);
+
public Geometry Geometry { get; }
///
@@ -57,6 +63,33 @@ namespace Avalonia.Direct2D1.Media
transform.ToDirect2D()),
this);
}
+
+ ///
+ public bool TryGetPointAtDistance(double distance, out Point point)
+ {
+ Geometry.ComputePointAtLength((float)distance, ContourApproximation, out var tangentVector);
+ point = new Point(tangentVector.X, tangentVector.Y);
+ return true;
+ }
+
+ ///
+ public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent)
+ {
+ // Direct2D doesnt have this sadly.
+ Logger.TryGet(LogEventLevel.Warning, LogArea.Visual)?.Log(this, "TryGetPointAndTangentAtDistance is not available in Direct2D.");
+ point = new Point();
+ tangent = new Point();
+ return false;
+ }
+
+ public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry)
+ {
+ // Direct2D doesnt have this too sadly.
+ Logger.TryGet(LogEventLevel.Warning, LogArea.Visual)?.Log(this, "TryGetSegment is not available in Direct2D.");
+
+ segmentGeometry = null;
+ return false;
+ }
protected virtual Geometry GetSourceGeometry() => Geometry;
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs
index 3261c45f15..fef6a9aa82 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs
@@ -1,4 +1,5 @@
using System;
+using System.IO;
using Avalonia.Platform;
using SharpDX.WIC;
using PixelFormat = Avalonia.Platform.PixelFormat;
@@ -7,11 +8,27 @@ namespace Avalonia.Direct2D1.Media.Imaging
{
class WriteableWicBitmapImpl : WicBitmapImpl, IWriteableBitmapImpl
{
+ public WriteableWicBitmapImpl(Stream stream, int decodeSize, bool horizontal,
+ Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode interpolationMode)
+ : base(stream, decodeSize, horizontal, interpolationMode)
+ {
+ }
+
public WriteableWicBitmapImpl(PixelSize size, Vector dpi, PixelFormat? pixelFormat, AlphaFormat? alphaFormat)
: base(size, dpi, pixelFormat, alphaFormat)
{
}
+ public WriteableWicBitmapImpl(Stream stream)
+ : base(stream)
+ {
+ }
+
+ public WriteableWicBitmapImpl(string fileName)
+ : base(fileName)
+ {
+ }
+
class LockedBitmap : ILockedFramebuffer
{
private readonly WriteableWicBitmapImpl _parent;
diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
index 7d86116f38..fe1d625efb 100644
--- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
+++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
@@ -147,13 +147,13 @@ namespace Avalonia.Win32.Interop.Wpf
{
var state = Keyboard.Modifiers;
var rv = default(RawInputModifiers);
- if (state.HasFlagCustom(ModifierKeys.Windows))
+ if (state.HasAllFlags(ModifierKeys.Windows))
rv |= RawInputModifiers.Meta;
- if (state.HasFlagCustom(ModifierKeys.Alt))
+ if (state.HasAllFlags(ModifierKeys.Alt))
rv |= RawInputModifiers.Alt;
- if (state.HasFlagCustom(ModifierKeys.Control))
+ if (state.HasAllFlags(ModifierKeys.Control))
rv |= RawInputModifiers.Control;
- if (state.HasFlagCustom(ModifierKeys.Shift))
+ if (state.HasAllFlags(ModifierKeys.Shift))
rv |= RawInputModifiers.Shift;
if (e != null)
{
diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs
index 5f02796d30..3066403186 100644
--- a/src/Windows/Avalonia.Win32/DataObject.cs
+++ b/src/Windows/Avalonia.Win32/DataObject.cs
@@ -181,7 +181,7 @@ namespace Avalonia.Win32
ole.GetData(ref format, out medium);
return;
}
- if(!format.tymed.HasFlagCustom(TYMED.TYMED_HGLOBAL))
+ if(!format.tymed.HasAllFlags(TYMED.TYMED_HGLOBAL))
Marshal.ThrowExceptionForHR(DV_E_TYMED);
if (format.dwAspect != DVASPECT.DVASPECT_CONTENT)
@@ -205,7 +205,7 @@ namespace Avalonia.Win32
return;
}
- if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasFlagCustom(TYMED.TYMED_HGLOBAL))
+ if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasAllFlags(TYMED.TYMED_HGLOBAL))
Marshal.ThrowExceptionForHR(DV_E_TYMED);
if (format.dwAspect != DVASPECT.DVASPECT_CONTENT)
@@ -228,7 +228,7 @@ namespace Avalonia.Win32
return ole.QueryGetData(ref format);
if (format.dwAspect != DVASPECT.DVASPECT_CONTENT)
return DV_E_DVASPECT;
- if (!format.tymed.HasFlagCustom(TYMED.TYMED_HGLOBAL))
+ if (!format.tymed.HasAllFlags(TYMED.TYMED_HGLOBAL))
return DV_E_TYMED;
string dataFormat = ClipboardFormats.GetFormat(format.cfFormat);
diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs
index d038e341b8..d8cb52a914 100644
--- a/src/Windows/Avalonia.Win32/OleDropTarget.cs
+++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs
@@ -24,11 +24,11 @@ namespace Avalonia.Win32
public static DropEffect ConvertDropEffect(DragDropEffects operation)
{
DropEffect result = DropEffect.None;
- if (operation.HasFlagCustom(DragDropEffects.Copy))
+ if (operation.HasAllFlags(DragDropEffects.Copy))
result |= DropEffect.Copy;
- if (operation.HasFlagCustom(DragDropEffects.Move))
+ if (operation.HasAllFlags(DragDropEffects.Move))
result |= DropEffect.Move;
- if (operation.HasFlagCustom(DragDropEffects.Link))
+ if (operation.HasAllFlags(DragDropEffects.Link))
result |= DropEffect.Link;
return result;
}
@@ -36,11 +36,11 @@ namespace Avalonia.Win32
public static DragDropEffects ConvertDropEffect(DropEffect effect)
{
DragDropEffects result = DragDropEffects.None;
- if (effect.HasFlagCustom(DropEffect.Copy))
+ if (effect.HasAllFlags(DropEffect.Copy))
result |= DragDropEffects.Copy;
- if (effect.HasFlagCustom(DropEffect.Move))
+ if (effect.HasAllFlags(DropEffect.Move))
result |= DragDropEffects.Move;
- if (effect.HasFlagCustom(DropEffect.Link))
+ if (effect.HasAllFlags(DropEffect.Link))
result |= DragDropEffects.Link;
return result;
}
@@ -50,17 +50,17 @@ namespace Avalonia.Win32
var modifiers = RawInputModifiers.None;
var state = (UnmanagedMethods.ModifierKeys)grfKeyState;
- if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_LBUTTON))
+ if (state.HasAllFlags(UnmanagedMethods.ModifierKeys.MK_LBUTTON))
modifiers |= RawInputModifiers.LeftMouseButton;
- if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
+ if (state.HasAllFlags(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
modifiers |= RawInputModifiers.MiddleMouseButton;
- if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_RBUTTON))
+ if (state.HasAllFlags(UnmanagedMethods.ModifierKeys.MK_RBUTTON))
modifiers |= RawInputModifiers.RightMouseButton;
- if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_SHIFT))
+ if (state.HasAllFlags(UnmanagedMethods.ModifierKeys.MK_SHIFT))
modifiers |= RawInputModifiers.Shift;
- if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_CONTROL))
+ if (state.HasAllFlags(UnmanagedMethods.ModifierKeys.MK_CONTROL))
modifiers |= RawInputModifiers.Control;
- if (state.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_ALT))
+ if (state.HasAllFlags(UnmanagedMethods.ModifierKeys.MK_ALT))
modifiers |= RawInputModifiers.Alt;
return modifiers;
}
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
index 31afcc368c..d6c71977d7 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
@@ -315,9 +315,9 @@ namespace Avalonia.Win32
{
Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
_owner,
- touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ?
+ touchInput.Flags.HasAllFlags(TouchInputFlags.TOUCHEVENTF_UP) ?
RawPointerEventType.TouchEnd :
- touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ?
+ touchInput.Flags.HasAllFlags(TouchInputFlags.TOUCHEVENTF_DOWN) ?
RawPointerEventType.TouchBegin :
RawPointerEventType.TouchUpdate,
PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
@@ -542,27 +542,27 @@ namespace Avalonia.Win32
var keys = (ModifierKeys)ToInt32(wParam);
var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
- if (keys.HasFlagCustom(ModifierKeys.MK_LBUTTON))
+ if (keys.HasAllFlags(ModifierKeys.MK_LBUTTON))
{
modifiers |= RawInputModifiers.LeftMouseButton;
}
- if (keys.HasFlagCustom(ModifierKeys.MK_RBUTTON))
+ if (keys.HasAllFlags(ModifierKeys.MK_RBUTTON))
{
modifiers |= RawInputModifiers.RightMouseButton;
}
- if (keys.HasFlagCustom(ModifierKeys.MK_MBUTTON))
+ if (keys.HasAllFlags(ModifierKeys.MK_MBUTTON))
{
modifiers |= RawInputModifiers.MiddleMouseButton;
}
- if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON1))
+ if (keys.HasAllFlags(ModifierKeys.MK_XBUTTON1))
{
modifiers |= RawInputModifiers.XButton1MouseButton;
}
- if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON2))
+ if (keys.HasAllFlags(ModifierKeys.MK_XBUTTON2))
{
modifiers |= RawInputModifiers.XButton2MouseButton;
}
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs
index 0a8648aa9a..d886b67241 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs
@@ -23,13 +23,13 @@ namespace Avalonia.Win32
AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0);
var borderThickness = new RECT();
- if (GetStyle().HasFlagCustom(WindowStyles.WS_THICKFRAME))
+ if (GetStyle().HasAllFlags(WindowStyles.WS_THICKFRAME))
{
AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle()), false, 0);
borderThickness.left *= -1;
borderThickness.top *= -1;
}
- else if (GetStyle().HasFlagCustom(WindowStyles.WS_BORDER))
+ else if (GetStyle().HasAllFlags(WindowStyles.WS_BORDER))
{
borderThickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 };
}
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index fa3d2665ed..877914fb1c 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -90,6 +90,7 @@ namespace Avalonia.Win32
private AutomationNode _automationNode;
private bool _isCloseRequested;
private bool _shown;
+ private bool _hiddenWindowIsParent;
public WindowImpl()
{
@@ -489,8 +490,8 @@ namespace Avalonia.Win32
IntPtr.Zero,
0,
0,
- requestedClientWidth + (windowRect.Width - clientRect.Width),
- requestedClientHeight + (windowRect.Height - clientRect.Height),
+ requestedClientWidth + (_isClientAreaExtended ? 0 : windowRect.Width - clientRect.Width),
+ requestedClientHeight + (_isClientAreaExtended ? 0 : windowRect.Height - clientRect.Height),
SetWindowPosFlags.SWP_RESIZE);
}
}
@@ -577,8 +578,7 @@ namespace Avalonia.Win32
public virtual void Show(bool activate)
{
- SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent != null ? _parent._hwnd : IntPtr.Zero);
-
+ SetParent(_parent);
ShowWindow(_showWindowState, activate);
}
@@ -587,7 +587,16 @@ namespace Avalonia.Win32
public void SetParent(IWindowImpl parent)
{
_parent = (WindowImpl)parent;
- SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent._hwnd);
+
+ var parentHwnd = _parent?._hwnd ?? IntPtr.Zero;
+
+ if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar)
+ {
+ parentHwnd = OffscreenParentWindow.Handle;
+ _hiddenWindowIsParent = true;
+ }
+
+ SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, parentHwnd);
}
public void SetEnabled(bool enable) => EnableWindow(_hwnd, enable);
@@ -844,7 +853,7 @@ namespace Avalonia.Win32
borderCaptionThickness.left *= -1;
borderCaptionThickness.top *= -1;
- bool wantsTitleBar = _extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.SystemChrome) || _extendTitleBarHint == -1;
+ bool wantsTitleBar = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) || _extendTitleBarHint == -1;
if (!wantsTitleBar)
{
@@ -861,7 +870,7 @@ namespace Avalonia.Win32
borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling);
}
- margins.cyTopHeight = _extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : 1;
+ margins.cyTopHeight = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : 1;
if (WindowState == WindowState.Maximized)
{
@@ -889,20 +898,19 @@ namespace Avalonia.Win32
_isClientAreaExtended = false;
return;
}
-
- GetWindowRect(_hwnd, out var rcClient);
+ GetClientRect(_hwnd, out var rcClient);
+ GetWindowRect(_hwnd, out var rcWindow);
// Inform the application of the frame change.
SetWindowPos(_hwnd,
- IntPtr.Zero,
- rcClient.left, rcClient.top,
- rcClient.Width, rcClient.Height,
- SetWindowPosFlags.SWP_FRAMECHANGED);
+ IntPtr.Zero,
+ rcWindow.left, rcWindow.top,
+ rcClient.Width, rcClient.Height,
+ SetWindowPosFlags.SWP_FRAMECHANGED);
if (_isClientAreaExtended && WindowState != WindowState.FullScreen)
{
var margins = UpdateExtendMargins();
-
DwmExtendFrameIntoClientArea(_hwnd, ref margins);
}
else
@@ -912,10 +920,12 @@ namespace Avalonia.Win32
_offScreenMargin = new Thickness();
_extendedMargins = new Thickness();
+
+ Resize(new Size(rcWindow.Width/ RenderScaling, rcWindow.Height / RenderScaling));
}
- if(!_isClientAreaExtended || (_extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.SystemChrome) &&
- !_extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.PreferSystemChrome)))
+ if(!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) &&
+ !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome)))
{
EnableCloseButton(_hwnd);
}
@@ -1100,16 +1110,38 @@ namespace Avalonia.Win32
if (newProperties.ShowInTaskbar)
{
exStyle |= WindowStyles.WS_EX_APPWINDOW;
+
+ if (_hiddenWindowIsParent)
+ {
+ // Can't enable the taskbar icon by clearing the parent window unless the window
+ // is hidden. Hide the window and show it again with the same activation state
+ // when we've finished. Interestingly it seems to work fine the other way.
+ var shown = IsWindowVisible(_hwnd);
+ var activated = GetActiveWindow() == _hwnd;
+
+ if (shown)
+ Hide();
+
+ _hiddenWindowIsParent = false;
+ SetParent(null);
+
+ if (shown)
+ Show(activated);
+ }
}
else
{
+ // To hide a non-owned window's taskbar icon we need to parent it to a hidden window.
+ if (_parent is null)
+ {
+ SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, OffscreenParentWindow.Handle);
+ _hiddenWindowIsParent = true;
+ }
+
exStyle &= ~WindowStyles.WS_EX_APPWINDOW;
}
SetExtendedStyle(exStyle);
-
- // TODO: To hide non-owned window from taskbar we need to parent it to a hidden window.
- // Otherwise it will still show in the taskbar.
}
WindowStyles style;
@@ -1259,7 +1291,7 @@ namespace Avalonia.Win32
public Action ExtendClientAreaToDecorationsChanged { get; set; }
///
- public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasFlagCustom(ExtendClientAreaChromeHints.PreferSystemChrome);
+ public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome);
///
public Thickness ExtendedMargins => _extendedMargins;
diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
index 7633a761a3..b01fb70f58 100644
--- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
+++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
@@ -330,6 +330,37 @@ namespace Avalonia.Animation.UnitTests
}
}
+ [Fact]
+ public void Transitions_Can_Be_Changed_To_Collection_That_Contains_The_Same_Transitions()
+ {
+ var target = CreateTarget();
+ var control = CreateControl(target.Object);
+
+ control.Transitions = new Transitions { target.Object };
+ }
+
+ [Fact]
+ public void Transitions_Can_Re_Set_During_Batch_Update()
+ {
+ var target = CreateTarget();
+ var control = CreateControl(target.Object);
+
+ // Assigning and then clearing Transitions ensures we have a transition state
+ // collection created.
+ control.Transitions = null;
+
+ control.BeginBatchUpdate();
+
+ // Setting opacity then Transitions means that we receive the Transitions change
+ // after the Opacity change when EndBatchUpdate is called.
+ control.Opacity = 0.5;
+ control.Transitions = new Transitions { target.Object };
+
+ // Which means that the transition state hasn't been initialized with the new
+ // Transitions when the Opacity change notification gets raised here.
+ control.EndBatchUpdate();
+ }
+
private static Mock CreateTarget()
{
return CreateTransition(Visual.OpacityProperty);
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
index 9f0e52c8d9..01d5752ead 100644
--- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
+++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
@@ -5,6 +5,7 @@ using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Data;
+using Avalonia.Layout;
using Xunit;
namespace Avalonia.Base.UnitTests
@@ -53,6 +54,21 @@ namespace Avalonia.Base.UnitTests
Assert.Empty(raised);
}
+ [Fact]
+ public void Binding_Disposal_Should_Not_Raise_Property_Changes_During_Batch_Update()
+ {
+ var target = new TestClass();
+ var observable = new TestObservable("foo");
+ var raised = new List();
+
+ var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+ target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
+ target.BeginBatchUpdate();
+ sub.Dispose();
+
+ Assert.Empty(raised);
+ }
+
[Fact]
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1()
{
@@ -89,6 +105,25 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("baz", target.Foo);
}
+ [Fact]
+ public void SetValue_Change_Should_Be_Raised_After_Batch_Update_3()
+ {
+ var target = new TestClass();
+ var raised = new List();
+
+ target.PropertyChanged += (s, e) => raised.Add(e);
+
+ target.BeginBatchUpdate();
+ target.SetValue(TestClass.BazProperty, Orientation.Horizontal, BindingPriority.LocalValue);
+ target.EndBatchUpdate();
+
+ Assert.Equal(1, raised.Count);
+ Assert.Equal(TestClass.BazProperty, raised[0].Property);
+ Assert.Equal(Orientation.Vertical, raised[0].OldValue);
+ Assert.Equal(Orientation.Horizontal, raised[0].NewValue);
+ Assert.Equal(Orientation.Horizontal, target.Baz);
+ }
+
[Fact]
public void SetValue_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update()
{
@@ -219,6 +254,26 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("baz", raised[0].NewValue);
}
+ [Fact]
+ public void Binding_Change_Should_Be_Raised_After_Batch_Update_3()
+ {
+ var target = new TestClass();
+ var observable = new TestObservable(Orientation.Horizontal);
+ var raised = new List();
+
+ target.PropertyChanged += (s, e) => raised.Add(e);
+
+ target.BeginBatchUpdate();
+ target.Bind(TestClass.BazProperty, observable, BindingPriority.LocalValue);
+ target.EndBatchUpdate();
+
+ Assert.Equal(1, raised.Count);
+ Assert.Equal(TestClass.BazProperty, raised[0].Property);
+ Assert.Equal(Orientation.Vertical, raised[0].OldValue);
+ Assert.Equal(Orientation.Horizontal, raised[0].NewValue);
+ Assert.Equal(Orientation.Horizontal, target.Baz);
+ }
+
[Fact]
public void Binding_Completion_Should_Be_Raised_After_Batch_Update()
{
@@ -240,6 +295,27 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(BindingPriority.Unset, raised[0].Priority);
}
+ [Fact]
+ public void Binding_Disposal_Should_Be_Raised_After_Batch_Update()
+ {
+ var target = new TestClass();
+ var observable = new TestObservable("foo");
+ var raised = new List();
+
+ var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+ target.PropertyChanged += (s, e) => raised.Add(e);
+
+ target.BeginBatchUpdate();
+ sub.Dispose();
+ target.EndBatchUpdate();
+
+ Assert.Equal(1, raised.Count);
+ Assert.Null(target.Foo);
+ Assert.Equal("foo", raised[0].OldValue);
+ Assert.Null(raised[0].NewValue);
+ Assert.Equal(BindingPriority.Unset, raised[0].Priority);
+ }
+
[Fact]
public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1()
{
@@ -449,6 +525,92 @@ namespace Avalonia.Base.UnitTests
Assert.Null(raised[1].NewValue);
}
+ [Fact]
+ public void Can_Set_Cleared_Value_When_Ending_Batch_Update()
+ {
+ var target = new TestClass();
+ var raised = 0;
+
+ target.Foo = "foo";
+
+ target.BeginBatchUpdate();
+ target.ClearValue(TestClass.FooProperty);
+ target.PropertyChanged += (sender, e) =>
+ {
+ if (e.Property == TestClass.FooProperty && e.NewValue is null)
+ {
+ target.Foo = "bar";
+ ++raised;
+ }
+ };
+ target.EndBatchUpdate();
+
+ Assert.Equal("bar", target.Foo);
+ Assert.Equal(1, raised);
+ }
+
+ [Fact]
+ public void Can_Bind_Cleared_Value_When_Ending_Batch_Update()
+ {
+ var target = new TestClass();
+ var raised = 0;
+ var notifications = new List();
+
+ target.Foo = "foo";
+
+ target.BeginBatchUpdate();
+ target.ClearValue(TestClass.FooProperty);
+ target.PropertyChanged += (sender, e) =>
+ {
+ if (e.Property == TestClass.FooProperty && e.NewValue is null)
+ {
+ target.Bind(TestClass.FooProperty, new TestObservable("bar"));
+ ++raised;
+ }
+
+ notifications.Add(e);
+ };
+ target.EndBatchUpdate();
+
+ Assert.Equal("bar", target.Foo);
+ Assert.Equal(1, raised);
+ Assert.Equal(2, notifications.Count);
+ Assert.Equal(null, notifications[0].NewValue);
+ Assert.Equal("bar", notifications[1].NewValue);
+ }
+
+ [Fact]
+ public void Can_Bind_Completed_Binding_Back_To_Original_Value_When_Ending_Batch_Update()
+ {
+ var target = new TestClass();
+ var raised = 0;
+ var notifications = new List();
+ var observable1 = new TestObservable("foo");
+ var observable2 = new TestObservable("foo");
+
+ target.Bind(TestClass.FooProperty, observable1);
+
+ target.BeginBatchUpdate();
+ observable1.OnCompleted();
+ target.PropertyChanged += (sender, e) =>
+ {
+ if (e.Property == TestClass.FooProperty && e.NewValue is null)
+ {
+ target.Bind(TestClass.FooProperty, observable2);
+ ++raised;
+ }
+
+ notifications.Add(e);
+ };
+ target.EndBatchUpdate();
+
+ Assert.Equal("foo", target.Foo);
+ Assert.Equal(1, raised);
+ Assert.Equal(2, notifications.Count);
+ Assert.Equal(null, notifications[0].NewValue);
+ Assert.Equal("foo", notifications[1].NewValue);
+ }
+
public class TestClass : AvaloniaObject
{
public static readonly StyledProperty FooProperty =
@@ -457,6 +619,9 @@ namespace Avalonia.Base.UnitTests
public static readonly StyledProperty BarProperty =
AvaloniaProperty.Register(nameof(Bar));
+ public static readonly StyledProperty BazProperty =
+ AvaloniaProperty.Register(nameof(Bar), Orientation.Vertical);
+
public string Foo
{
get => GetValue(FooProperty);
@@ -468,6 +633,12 @@ namespace Avalonia.Base.UnitTests
get => GetValue(BarProperty);
set => SetValue(BarProperty, value);
}
+
+ public Orientation Baz
+ {
+ get => GetValue(BazProperty);
+ set => SetValue(BazProperty, value);
+ }
}
public class TestObservable : ObservableBase>
diff --git a/tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs b/tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs
new file mode 100644
index 0000000000..f74457a924
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Runtime.CompilerServices;
+using Avalonia.Animation;
+using Avalonia.Animation.Animators;
+using Avalonia.Layout;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Animations
+{
+ [MemoryDiagnoser]
+ public class TransitionBenchmark
+ {
+ private readonly AddValueObserver _observer;
+ private readonly List _producedValues;
+ private readonly Subject _timeProducer;
+ private readonly DoubleTransition _transition;
+
+ public TransitionBenchmark()
+ {
+ _transition = new DoubleTransition
+ {
+ Duration = TimeSpan.FromMilliseconds(FrameCount), Property = Layoutable.WidthProperty
+ };
+
+ _timeProducer = new Subject();
+ _producedValues = new List(FrameCount);
+
+ _observer = new AddValueObserver(_producedValues);
+ }
+
+ [Params(10, 100)] public int FrameCount { get; set; }
+
+ [Benchmark]
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public void NewTransition()
+ {
+ var transitionObs = _transition.DoTransition(_timeProducer, 0, 1);
+
+ _producedValues.Clear();
+
+ using var transitionSub = transitionObs.Subscribe(_observer);
+
+ for (int i = 0; i < FrameCount; i++)
+ {
+ _timeProducer.OnNext(i / 1000d);
+ }
+
+ Debug.Assert(_producedValues.Count == FrameCount);
+ }
+
+ private class AddValueObserver : IObserver
+ {
+ private readonly List _values;
+
+ public AddValueObserver(List values)
+ {
+ _values = values;
+ }
+
+ public void OnCompleted()
+ {
+ }
+
+ public void OnError(Exception error)
+ {
+ }
+
+ public void OnNext(double value)
+ {
+ _values.Add(value);
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
index 1570205456..8792cb5cb1 100644
--- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
+++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
@@ -61,6 +61,28 @@ namespace Avalonia.Benchmarks
throw new NotImplementedException();
}
+ public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(string fileName)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream)
+ {
+ throw new NotImplementedException();
+ }
+
public IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride)
{
throw new NotImplementedException();
@@ -86,10 +108,8 @@ namespace Avalonia.Benchmarks
return new MockFontManagerImpl();
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
- width = default;
-
return new NullGlyphRun();
}
diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs
new file mode 100644
index 0000000000..79a11d0cea
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs
@@ -0,0 +1,63 @@
+using System;
+
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml.Styling;
+using Avalonia.Shared.PlatformSupport;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
+
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Themes
+{
+ [MemoryDiagnoser]
+ public class ThemeBenchmark : IDisposable
+ {
+ private IDisposable _app;
+
+ public ThemeBenchmark()
+ {
+ AssetLoader.RegisterResUriParsers();
+
+ _app = UnitTestApplication.Start(TestServices.StyledWindow.With(theme: () => null));
+ // Add empty style to override it later
+ UnitTestApplication.Current.Styles.Add(new Style());
+ }
+
+ [Benchmark]
+ [Arguments("avares://Avalonia.Themes.Fluent/FluentDark.xaml")]
+ [Arguments("avares://Avalonia.Themes.Fluent/FluentLight.xaml")]
+ public bool InitFluentTheme(string themeUri)
+ {
+ UnitTestApplication.Current.Styles[0] = new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Benchmarks"))
+ {
+ Source = new Uri(themeUri)
+ };
+ return ((IResourceHost)UnitTestApplication.Current).TryGetResource("SystemAccentColor", out _);
+ }
+
+ [Benchmark]
+ [Arguments("avares://Avalonia.Themes.Default/Accents/BaseLight.xaml")]
+ [Arguments("avares://Avalonia.Themes.Default/Accents/BaseDark.xaml")]
+ public bool InitDefaultTheme(string themeUri)
+ {
+ UnitTestApplication.Current.Styles[0] = new Styles
+ {
+ new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Benchmarks"))
+ {
+ Source = new Uri(themeUri)
+ },
+ new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Benchmarks"))
+ {
+ Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml")
+ }
+ };
+ return ((IResourceHost)UnitTestApplication.Current).TryGetResource("ThemeAccentColor", out _);
+ }
+
+ public void Dispose()
+ {
+ _app.Dispose();
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs
index 7ad0e480c6..c9ad24e654 100644
--- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs
@@ -269,6 +269,19 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, command.SubscriptionCount);
}
+ [Fact]
+ public void Button_Invokes_CanExecute_When_CommandParameter_Changed()
+ {
+ var command = new TestCommand(p => p is bool value && value);
+ var target = new Button { Command = command };
+
+ target.CommandParameter = true;
+ Assert.True(target.IsEffectivelyEnabled);
+
+ target.CommandParameter = false;
+ Assert.False(target.IsEffectivelyEnabled);
+ }
+
private class TestButton : Button, IRenderRoot
{
public TestButton()
@@ -324,12 +337,22 @@ namespace Avalonia.Controls.UnitTests
private class TestCommand : ICommand
{
+ private readonly Func _canExecute;
+ private readonly Action _execute;
private EventHandler _canExecuteChanged;
- private bool _enabled;
+ private bool _enabled = true;
- public TestCommand(bool enabled)
+ public TestCommand(bool enabled = true)
{
_enabled = enabled;
+ _canExecute = _ => _enabled;
+ _execute = _ => { };
+ }
+
+ public TestCommand(Func canExecute, Action execute = null)
+ {
+ _canExecute = canExecute;
+ _execute = execute ?? (_ => { });
}
public bool IsEnabled
@@ -353,11 +376,9 @@ namespace Avalonia.Controls.UnitTests
remove { _canExecuteChanged -= value; --SubscriptionCount; }
}
- public bool CanExecute(object parameter) => _enabled;
+ public bool CanExecute(object parameter) => _canExecute(parameter);
- public void Execute(object parameter)
- {
- }
+ public void Execute(object parameter) => _execute(parameter);
}
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
index 051f6c3fd3..b93e48618d 100644
--- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
@@ -1,10 +1,14 @@
using System.Collections.ObjectModel;
using System.Linq;
+using System.Reactive.Subjects;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
+using Avalonia.Data;
using Avalonia.LogicalTree;
+using Avalonia.Threading;
using Avalonia.VisualTree;
+using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
@@ -77,9 +81,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection
{
- "Foo",
- "Bar",
- "FooBar"
+ "Foo",
+ "Bar",
+ "FooBar"
};
var target = new Carousel
@@ -113,9 +117,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection
{
- "Foo",
- "Bar",
- "FooBar"
+ "Foo",
+ "Bar",
+ "FooBar"
};
var target = new Carousel
@@ -172,9 +176,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection
{
- "Foo",
- "Bar",
- "FooBar"
+ "Foo",
+ "Bar",
+ "FooBar"
};
var target = new Carousel
@@ -206,9 +210,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection
{
- "Foo",
- "Bar",
- "FooBar"
+ "Foo",
+ "Bar",
+ "FooBar"
};
var target = new Carousel
@@ -235,9 +239,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection
{
- "Foo",
- "Bar",
- "FooBar"
+ "Foo",
+ "Bar",
+ "FooBar"
};
var target = new Carousel
@@ -269,9 +273,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection
{
- "Foo",
- "Bar",
- "FooBar"
+ "Foo",
+ "Bar",
+ "FooBar"
};
var target = new Carousel
@@ -311,5 +315,29 @@ namespace Avalonia.Controls.UnitTests
contentPresenter.UpdateChild();
return Assert.IsType(contentPresenter.Child);
}
+
+ [Fact]
+ public void SelectedItem_Validation()
+ {
+ using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+ {
+ var target = new Carousel
+ {
+ Template = new FuncControlTemplate(CreateTemplate), IsVirtualized = false
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ var exception = new System.InvalidCastException("failed validation");
+ var textObservable =
+ new BehaviorSubject(new BindingNotification(exception,
+ BindingErrorType.DataValidationError));
+ target.Bind(ComboBox.SelectedItemProperty, textObservable);
+
+ Assert.True(DataValidationErrors.GetHasErrors(target));
+ Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
+ }
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
index 4ea838358c..8f9c7fdb0b 100644
--- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
@@ -1,11 +1,14 @@
using System.Linq;
+using System.Reactive.Subjects;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
+using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Media;
+using Avalonia.Threading;
using Avalonia.UnitTests;
using Xunit;
@@ -173,5 +176,31 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(expectedSelectedIndex, target.SelectedIndex);
}
}
+
+ [Fact]
+ public void SelectedItem_Validation()
+ {
+
+ using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+ {
+ var target = new ComboBox
+ {
+ Template = GetTemplate(),
+ VirtualizationMode = ItemVirtualizationMode.None
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ var exception = new System.InvalidCastException("failed validation");
+ var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError));
+ target.Bind(ComboBox.SelectedItemProperty, textObservable);
+
+ Assert.True(DataValidationErrors.GetHasErrors(target));
+ Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
+
+ }
+
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
new file mode 100644
index 0000000000..0dce73180e
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
@@ -0,0 +1,325 @@
+using System;
+using System.Linq;
+using Avalonia.Input;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+ public class FlyoutTests
+ {
+ [Fact]
+ public void Opening_Raises_Single_Opening_Event()
+ {
+ using (CreateServicesWithFocus())
+ {
+ var window = PreparedWindow();
+ window.Show();
+
+ int tracker = 0;
+ Flyout f = new Flyout();
+ f.Opening += (s, e) =>
+ {
+ tracker++;
+ };
+ f.ShowAt(window);
+
+ Assert.Equal(1, tracker);
+ }
+ }
+
+ [Fact]
+ public void Opening_Raises_Single_Opened_Event()
+ {
+ using (CreateServicesWithFocus())
+ {
+ var window = PreparedWindow();
+ window.Show();
+
+ int tracker = 0;
+ Flyout f = new Flyout();
+ f.Opened += (s, e) =>
+ {
+ tracker++;
+ };
+ f.ShowAt(window);
+
+ Assert.Equal(1, tracker);
+ }
+ }
+
+ [Fact]
+ public void Closing_Raises_Single_Closing_Event()
+ {
+ using (CreateServicesWithFocus())
+ {
+ var window = PreparedWindow();
+ window.Show();
+
+ int tracker = 0;
+ Flyout f = new Flyout();
+ f.Closing += (s, e) =>
+ {
+ tracker++;
+ };
+ f.ShowAt(window);
+ f.Hide();
+
+ Assert.Equal(1, tracker);
+ }
+ }
+
+ [Fact]
+ public void Closing_Raises_Single_Closed_Event()
+ {
+ using (CreateServicesWithFocus())
+ {
+ var window = PreparedWindow();
+ window.Show();
+
+ int tracker = 0;
+ Flyout f = new Flyout();
+ f.Closed += (s, e) =>
+ {
+ tracker++;
+ };
+ f.ShowAt(window);
+ f.Hide();
+
+ Assert.Equal(1, tracker);
+ }
+ }
+
+ [Fact]
+ public void Cancel_Closing_Keeps_Flyout_Open()
+ {
+ using (CreateServicesWithFocus())
+ {
+ var window = PreparedWindow();
+ window.Show();
+
+ int tracker = 0;
+ Flyout f = new Flyout();
+ f.Closing += (s, e) =>
+ {
+ e.Cancel = true;
+ };
+ f.ShowAt(window);
+ f.Hide();
+
+ Assert.True(f.IsOpen);
+ }
+ }
+
+ [Fact]
+ public void Flyout_Has_Uncancellable_Close_Before_Showing_On_A_Different_Target()
+ {
+ using (CreateServicesWithFocus())
+ {
+ var window = PreparedWindow();
+ Button target1 = new Button();
+ Button target2 = new Button();
+
+ window.Content = new StackPanel
+ {
+ Children =
+ {
+ target1,
+ target2
+ }
+ };
+ window.Show();
+
+ bool closingFired = false;
+ bool closedFired = false;
+ Flyout f = new Flyout();
+ f.Closing += (s, e) =>
+ {
+ closingFired = true; //This shouldn't happen
+ };
+ f.Closed += (s, e) =>
+ {
+ closedFired = true;
+ };
+
+ f.ShowAt(target1);
+
+ f.ShowAt(target2);
+
+ Assert.False(closingFired);
+ Assert.True(closedFired);
+ }
+ }
+
+ [Fact]
+ public void ShowMode_Standard_Attemps_Focus_Flyout_Content()
+ {
+ using (CreateServicesWithFocus())
+ {
+ var window = PreparedWindow();
+
+ var flyoutTextBox = new TextBox();
+ var button = new Button
+ {
+ Flyout = new Flyout
+ {
+ ShowMode = FlyoutShowMode.Standard,
+ Content = new Panel
+ {
+ Children =
+ {
+ flyoutTextBox
+ }
+ }
+ }
+ };
+
+ window.Content = button;
+ window.Show();
+
+ button.Focus();
+ Assert.True(FocusManager.Instance?.Current == button);
+ button.Flyout.ShowAt(button);
+ Assert.False(button.IsFocused);
+ Assert.True(FocusManager.Instance?.Current == flyoutTextBox);
+ }
+ }
+
+ [Fact]
+ public void ShowMode_Transient_Does_Not_Move_Focus_From_Target()
+ {
+ using (CreateServicesWithFocus())
+ {
+ var window = PreparedWindow();
+
+ var flyoutTextBox = new TextBox();
+ var button = new Button
+ {
+ Flyout = new Flyout
+ {
+ ShowMode = FlyoutShowMode.Transient,
+ Content = new Panel
+ {
+ Children =
+ {
+ flyoutTextBox
+ }
+ }
+ },
+ Content = "Test"
+ };
+
+ window.Content = button;
+ window.Show();
+
+ FocusManager.Instance?.Focus(button);
+ Assert.True(FocusManager.Instance?.Current == button);
+ button.Flyout.ShowAt(button);
+ Assert.True(FocusManager.Instance?.Current == button);
+ }
+ }
+
+ [Fact]
+ public void ContextFlyout_Can_Be_Set_In_Styles()
+ {
+ using (CreateServicesWithFocus())
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+
+
+";
+
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var target1 = window.Find("target1");
+ var target2 = window.Find("target2");
+ var mouse = new MouseTestHelper();
+
+ Assert.NotNull(target1.ContextFlyout);
+ Assert.NotNull(target2.ContextFlyout);
+ Assert.Same(target1.ContextFlyout, target2.ContextFlyout);
+
+ window.Show();
+
+ var menu = target1.ContextFlyout;
+ mouse.Click(target1, MouseButton.Right);
+ Assert.True(menu.IsOpen);
+ mouse.Click(target2, MouseButton.Right);
+ Assert.True(menu.IsOpen);
+ }
+ }
+
+ [Fact]
+ public void Setting_FlyoutPresenterClasses_Sets_Classes_On_FlyoutPresenter()
+ {
+ using (CreateServicesWithFocus())
+ {
+ var xaml = @"
+
+
+
+
+";
+
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var flyoutPanel = new Panel();
+ var button = new Button
+ {
+ Content = "Test",
+ Flyout = new Flyout
+ {
+ Content = flyoutPanel
+ }
+ };
+ window.Content = button;
+ window.Show();
+
+ (button.Flyout as Flyout).FlyoutPresenterClasses.Add("TestClass");
+
+ button.Flyout.ShowAt(button);
+
+ var presenter = flyoutPanel.GetVisualAncestors().OfType().FirstOrDefault();
+ Assert.NotNull(presenter);
+ Assert.True((presenter.Background as ISolidColorBrush).Color == Colors.Red);
+ }
+ }
+
+ private IDisposable CreateServicesWithFocus()
+ {
+ return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform:
+ new MockWindowingPlatform(null,
+ x =>
+ {
+ return MockWindowingPlatform.CreatePopupMock(x).Object;
+ }),
+ focusManager: new FocusManager(),
+ keyboardDevice: () => new KeyboardDevice()));
+ }
+
+ private Window PreparedWindow(object content = null)
+ {
+ var w = new Window { Content = content };
+ w.ApplyTemplate();
+ return w;
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
index 684486cbae..bfece7871c 100644
--- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
@@ -379,6 +379,17 @@ namespace Avalonia.Controls.UnitTests
Assert.DoesNotContain(":empty", target.Classes);
}
+ [Fact]
+ public void Empty_Class_Should_Be_Set_When_Items_Not_Set()
+ {
+ var target = new ItemsControl()
+ {
+ Template = GetTemplate(),
+ };
+
+ Assert.Contains(":empty", target.Classes);
+ }
+
[Fact]
public void Empty_Class_Should_Be_Set_When_Empty_Collection_Set()
{
@@ -393,6 +404,118 @@ namespace Avalonia.Controls.UnitTests
Assert.Contains(":empty", target.Classes);
}
+ [Fact]
+ public void Item_Count_Should_Be_Set_When_Items_Added()
+ {
+ var target = new ItemsControl()
+ {
+ Template = GetTemplate(),
+ Items = new[] { 1, 2, 3 },
+ };
+
+ Assert.Equal(3, target.ItemCount);
+ }
+
+ [Fact]
+ public void Item_Count_Should_Be_Set_When_Items_Changed()
+ {
+ var items = new ObservableCollection() { 1, 2, 3 };
+
+ var target = new ItemsControl()
+ {
+ Template = GetTemplate(),
+ Items = items,
+ };
+
+ items.Add(4);
+
+ Assert.Equal(4, target.ItemCount);
+
+ items.Clear();
+
+ Assert.Equal(0, target.ItemCount);
+ }
+
+ [Fact]
+ public void Empty_Class_Should_Be_Set_When_Items_Collection_Cleared()
+ {
+ var items = new ObservableCollection() { 1, 2, 3 };
+
+ var target = new ItemsControl()
+ {
+ Template = GetTemplate(),
+ Items = items,
+ };
+
+ items.Clear();
+
+ Assert.Contains(":empty", target.Classes);
+ }
+
+ [Fact]
+ public void Empty_Class_Should_Not_Be_Set_When_Items_Collection_Count_Increases()
+ {
+ var items = new ObservableCollection() { };
+
+ var target = new ItemsControl()
+ {
+ Template = GetTemplate(),
+ Items = items,
+ };
+
+ items.Add(1);
+
+ Assert.DoesNotContain(":empty", target.Classes);
+ }
+
+ [Fact]
+ public void Single_Item_Class_Should_Be_Set_When_Items_Collection_Count_Increases_To_One()
+ {
+ var items = new ObservableCollection() { };
+
+ var target = new ItemsControl()
+ {
+ Template = GetTemplate(),
+ Items = items,
+ };
+
+ items.Add(1);
+
+ Assert.Contains(":singleitem", target.Classes);
+ }
+
+ [Fact]
+ public void Empty_Class_Should_Not_Be_Set_When_Items_Collection_Cleared()
+ {
+ var items = new ObservableCollection() { 1, 2, 3 };
+
+ var target = new ItemsControl()
+ {
+ Template = GetTemplate(),
+ Items = items,
+ };
+
+ items.Clear();
+
+ Assert.DoesNotContain(":singleitem", target.Classes);
+ }
+
+ [Fact]
+ public void Single_Item_Class_Should_Not_Be_Set_When_Items_Collection_Count_Increases_Beyond_One()
+ {
+ var items = new ObservableCollection() { 1 };
+
+ var target = new ItemsControl()
+ {
+ Template = GetTemplate(),
+ Items = items,
+ };
+
+ items.Add(2);
+
+ Assert.DoesNotContain(":singleitem", target.Classes);
+ }
+
[Fact]
public void Setting_Presenter_Explicitly_Should_Set_Item_Parent()
{
diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
index 145fce4fed..7c57e22933 100644
--- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
@@ -1,11 +1,15 @@
+using System.Collections.ObjectModel;
using System.Linq;
+using System.Reactive.Subjects;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
+using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Styling;
+using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
@@ -454,6 +458,33 @@ namespace Avalonia.Controls.UnitTests
}
}
+ [Fact]
+ public void Initial_Binding_Of_SelectedItems_Should_Not_Cause_Write_To_SelectedItems()
+ {
+ var target = new ListBox
+ {
+ [!ListBox.ItemsProperty] = new Binding("Items"),
+ [!ListBox.SelectedItemsProperty] = new Binding("SelectedItems"),
+ };
+
+ var viewModel = new
+ {
+ Items = new[] { "Foo", "Bar", "Baz " },
+ SelectedItems = new ObservableCollection { "Bar" },
+ };
+
+ var raised = 0;
+
+ viewModel.SelectedItems.CollectionChanged += (s, e) => ++raised;
+
+ target.DataContext = viewModel;
+
+ Assert.Equal(0, raised);
+ Assert.Equal(new[] { "Bar" }, viewModel.SelectedItems);
+ Assert.Equal(new[] { "Bar" }, target.SelectedItems);
+ Assert.Equal(new[] { "Bar" }, target.Selection.SelectedItems);
+ }
+
private FuncControlTemplate ListBoxTemplate()
{
return new FuncControlTemplate((parent, scope) =>
@@ -559,5 +590,28 @@ namespace Avalonia.Controls.UnitTests
public string Value { get; }
}
+
+
+ [Fact]
+ public void SelectedItem_Validation()
+ {
+ var target = new ListBox
+ {
+ Template = ListBoxTemplate(),
+ Items = new[] { "Foo" },
+ ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()),
+ SelectionMode = SelectionMode.AlwaysSelected,
+ VirtualizationMode = ItemVirtualizationMode.None
+ };
+
+ Prepare(target);
+
+ var exception = new System.InvalidCastException("failed validation");
+ var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError));
+ target.Bind(ComboBox.SelectedItemProperty, textObservable);
+
+ Assert.True(DataValidationErrors.GetHasErrors(target));
+ Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
index ab21c5d330..c3d35653cc 100644
--- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
@@ -37,7 +37,7 @@ namespace Avalonia.Controls.UnitTests
target.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
target.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
- Assert.Equal(new[] { true, false, true }, values);
+ Assert.Equal(new[] { false, true }, values);
}
[Fact]
diff --git a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs
index ce3e698cf3..224b89bf85 100644
--- a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs
@@ -97,6 +97,7 @@ namespace Avalonia.Controls.UnitTests.Selection
target.WritableSelectedItems.Clear();
Assert.Empty(target.SelectedIndexes);
+ Assert.Empty(target.WritableSelectedItems);
}
[Fact]
@@ -123,6 +124,7 @@ namespace Avalonia.Controls.UnitTests.Selection
target.WritableSelectedItems = null;
Assert.Empty(target.SelectedIndexes);
+ Assert.Empty(target.WritableSelectedItems);
}
[Fact]
@@ -182,6 +184,7 @@ namespace Avalonia.Controls.UnitTests.Selection
target.Source = items;
Assert.Equal(1, target.SelectedIndex);
+ Assert.Equal(new[] { "bar" }, target.WritableSelectedItems);
}
[Fact]
@@ -203,6 +206,7 @@ namespace Avalonia.Controls.UnitTests.Selection
items.Reset(new[] { "baz", "foo", "bar" });
Assert.Equal(2, target.SelectedIndex);
+ Assert.Equal(new[] { "bar" }, target.WritableSelectedItems);
}
[Fact]
@@ -227,6 +231,7 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal(-1, target.SelectedIndex);
Assert.Equal(null, target.SelectedItem);
+ Assert.Empty(target.WritableSelectedItems);
Assert.Contains(nameof(target.SelectedIndex), changed);
Assert.Contains(nameof(target.SelectedItem), changed);
@@ -246,6 +251,7 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal("foo", target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
+ Assert.Equal(new[] { "foo" }, target.WritableSelectedItems);
}
[Fact]
@@ -257,6 +263,7 @@ namespace Avalonia.Controls.UnitTests.Selection
target.Source = new[] { "baz", "foo", "bar" };
Assert.Equal(2, target.SelectedIndex);
+ Assert.Equal(new[] { "bar" }, target.WritableSelectedItems);
}
private static InternalSelectionModel CreateTarget(
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index 9f09c19110..64c0020f92 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Presenters;
@@ -56,7 +57,44 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal("123", target1.SelectedText);
}
}
-
+
+ [Fact]
+ public void Opening_Context_Flyout_Does_not_Lose_Selection()
+ {
+ using (UnitTestApplication.Start(FocusServices))
+ {
+ var target1 = new TextBox
+ {
+ Template = CreateTemplate(),
+ Text = "1234",
+ ContextFlyout = new MenuFlyout
+ {
+ Items = new List
+ {
+ new MenuItem { Header = "Item 1" },
+ new MenuItem {Header = "Item 2" },
+ new MenuItem {Header = "Item 3" }
+ }
+ }
+ };
+
+
+ target1.ApplyTemplate();
+
+ var root = new TestRoot() { Child = target1 };
+
+ target1.SelectionStart = 0;
+ target1.SelectionEnd = 3;
+
+ target1.Focus();
+ Assert.True(target1.IsFocused);
+
+ target1.ContextFlyout.ShowAt(target1);
+
+ Assert.Equal("123", target1.SelectedText);
+ }
+ }
+
[Fact]
public void DefaultBindingMode_Should_Be_TwoWay()
{
@@ -340,7 +378,7 @@ namespace Avalonia.Controls.UnitTests
[Theory]
[InlineData(new object[] { false, TextWrapping.NoWrap, ScrollBarVisibility.Hidden })]
- [InlineData(new object[] { false, TextWrapping.Wrap, ScrollBarVisibility.Hidden })]
+ [InlineData(new object[] { false, TextWrapping.Wrap, ScrollBarVisibility.Disabled })]
[InlineData(new object[] { true, TextWrapping.NoWrap, ScrollBarVisibility.Auto })]
[InlineData(new object[] { true, TextWrapping.Wrap, ScrollBarVisibility.Disabled })]
public void Has_Correct_Horizontal_ScrollBar_Visibility(
diff --git a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs
index e005bafbf9..7eaec35506 100644
--- a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs
@@ -114,5 +114,61 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(2.0, scaleTransform.ScaleX);
Assert.Equal(2.0, scaleTransform.ScaleY);
}
+
+ [Theory]
+ [InlineData(50, 100, 50, 100, 50, 100, 1)]
+ [InlineData(50, 100, 150, 150, 50, 100, 1)]
+ [InlineData(50, 100, 25, 50, 25, 50, 0.5)]
+ public void Viewbox_Should_Return_Correct_SizeAndScale_StretchDirection_DownOnly(
+ double childWidth, double childHeight,
+ double viewboxWidth, double viewboxHeight,
+ double expectedWidth, double expectedHeight,
+ double expectedScale)
+ {
+ var target = new Viewbox
+ {
+ Child = new Control { Width = childWidth, Height = childHeight },
+ StretchDirection = StretchDirection.DownOnly
+ };
+
+ target.Measure(new Size(viewboxWidth, viewboxHeight));
+ target.Arrange(new Rect(default, target.DesiredSize));
+
+ Assert.Equal(new Size(expectedWidth, expectedHeight), target.DesiredSize);
+
+ var scaleTransform = target.Child.RenderTransform as ScaleTransform;
+
+ Assert.NotNull(scaleTransform);
+ Assert.Equal(expectedScale, scaleTransform.ScaleX);
+ Assert.Equal(expectedScale, scaleTransform.ScaleY);
+ }
+
+ [Theory]
+ [InlineData(50, 100, 50, 100, 50, 100, 1)]
+ [InlineData(50, 100, 25, 50, 25, 50, 1)]
+ [InlineData(50, 100, 150, 150, 75, 150, 1.5)]
+ public void Viewbox_Should_Return_Correct_SizeAndScale_StretchDirection_UpOnly(
+ double childWidth, double childHeight,
+ double viewboxWidth, double viewboxHeight,
+ double expectedWidth, double expectedHeight,
+ double expectedScale)
+ {
+ var target = new Viewbox
+ {
+ Child = new Control { Width = childWidth, Height = childHeight },
+ StretchDirection = StretchDirection.UpOnly
+ };
+
+ target.Measure(new Size(viewboxWidth, viewboxHeight));
+ target.Arrange(new Rect(default, target.DesiredSize));
+
+ Assert.Equal(new Size(expectedWidth, expectedHeight), target.DesiredSize);
+
+ var scaleTransform = target.Child.RenderTransform as ScaleTransform;
+
+ Assert.NotNull(scaleTransform);
+ Assert.Equal(expectedScale, scaleTransform.ScaleX);
+ Assert.Equal(expectedScale, scaleTransform.ScaleY);
+ }
}
}
diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs
index f1cec24516..178855d71c 100644
--- a/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs
+++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs
@@ -162,7 +162,7 @@ namespace Avalonia.Layout.UnitTests
{
var root = CreateRoot();
var target = new Canvas { Width = 200, Height = 200 };
- var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate() };
+ var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate(), HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden };
var raised = 0;
target.EffectiveViewportChanged += (s, e) =>
@@ -185,7 +185,7 @@ namespace Avalonia.Layout.UnitTests
{
var root = CreateRoot();
var target = new Canvas { Width = 200, Height = 200 };
- var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate() };
+ var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate(), HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden };
var raised = 0;
root.Child = scroller;
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
index 4f2b580bce..8af638c5d7 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
@@ -377,5 +377,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
public string Greeting1 { get; set; } = "Hello";
public string Greeting2 { get; set; } = "World";
}
+
+ [Fact]
+ public void Binding_Classes_Works()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ // Note, this test also checks `Classes` reordering, so it should be kept AFTER the last single class
+ var xaml = @"
+
+
+";
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var button = window.FindControl("button");
+
+ button.DataContext = new { Foo = true };
+ window.ApplyTemplate();
+
+ Assert.True(button.Classes.Contains("MyClass"));
+ Assert.True(button.Classes.Contains("MySecondClass"));
+ Assert.True(button.Classes.Contains("foo"));
+ Assert.True(button.Classes.Contains("bar"));
+
+ button.DataContext = new { Foo = false };
+
+ Assert.False(button.Classes.Contains("MyClass"));
+ Assert.True(button.Classes.Contains("MySecondClass"));
+ Assert.True(button.Classes.Contains("foo"));
+ Assert.True(button.Classes.Contains("bar"));
+ }
+ }
}
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
index 033b670bf4..53881467e7 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
@@ -7,6 +7,31 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
public class DataTemplateTests : XamlTestBase
{
+ [Fact]
+ public void DataTemplate_Can_Be_Empty()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+";
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var target = window.FindControl("target");
+
+ window.ApplyTemplate();
+ target.ApplyTemplate();
+ ((ContentPresenter)target.Presenter).UpdateChild();
+
+ Assert.Null(target.Presenter.Child);
+ }
+ }
+
[Fact]
public void DataTemplate_Can_Contain_Name()
{
diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
index 42ec392066..304b8c5cfd 100644
--- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
+++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
@@ -14,10 +14,6 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
public class VisualBrushTests : TestBase
{
- //Whitespaces are used here to be able to compare rendering results in a platform independent way.
- //Otherwise tests will fail because of slightly different glyph rendering.
- private static readonly string s_visualBrushText = " ";
-
public VisualBrushTests()
: base(@"Media\VisualBrush")
{
@@ -46,13 +42,11 @@ namespace Avalonia.Direct2D1.RenderTests.Media
BorderThickness = new Thickness(2),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
- Child = new TextBlock
+ Child = new Panel
{
- FontSize = 24,
- FontFamily = TestFontFamily,
- Background = Brushes.Green,
- Foreground = Brushes.Yellow,
- Text = s_visualBrushText
+ Height = 26,
+ Width = 150,
+ Background = Brushes.Green
}
}
}
@@ -392,10 +386,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
Background = Brushes.Yellow,
HorizontalAlignment = HorizontalAlignment.Left,
- Child = new TextBlock
+ Child = new Panel
{
- FontFamily = TestFontFamily,
- Text = s_visualBrushText
+ Height = 10,
+ Width = 50
}
}),
new Border
diff --git a/tests/Avalonia.Skia.UnitTests/DrawingContextImplTests.cs b/tests/Avalonia.Skia.UnitTests/DrawingContextImplTests.cs
new file mode 100644
index 0000000000..df0cc2fc1a
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/DrawingContextImplTests.cs
@@ -0,0 +1,30 @@
+using Avalonia.Media;
+using Avalonia.Skia.Helpers;
+using SkiaSharp;
+using Xunit;
+
+namespace Avalonia.Skia.UnitTests
+{
+ public class DrawingContextImplTests
+ {
+ [Fact]
+ public void DrawLine_With_Zero_Thickness_Pen_Does_Not_Throw()
+ {
+ var target = CreateTarget();
+ target.DrawLine(new Pen(Brushes.Black, 0), new Point(0, 0), new Point(10, 10));
+ }
+
+ [Fact]
+ public void DrawRectangle_With_Zero_Thickness_Pen_Does_Not_Throw()
+ {
+ var target = CreateTarget();
+ target.DrawRectangle(Brushes.Black, new Pen(Brushes.Black, 0), new RoundedRect(new Rect(0, 0, 100, 100), new CornerRadius(4)));
+ }
+
+ private DrawingContextImpl CreateTarget()
+ {
+ var canvas = new SKCanvas(new SKBitmap(100, 100));
+ return (DrawingContextImpl)DrawingContextHelper.WrapSkiaCanvas(canvas, new Vector(96, 96));
+ }
+ }
+}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
new file mode 100644
index 0000000000..f9c45e7d22
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Globalization;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Skia.UnitTests.Media
+{
+ public class GlyphRunTests
+ {
+ [InlineData("ABC \r", 29, 4, 1)]
+ [InlineData("ABC \r", 23, 3, 1)]
+ [InlineData("ABC \r", 17, 2, 1)]
+ [InlineData("ABC \r", 11, 1, 1)]
+ [InlineData("ABC \r", 7, 1, 0)]
+ [InlineData("ABC \r", 5, 0, 1)]
+ [InlineData("ABC \r", 2, 0, 0)]
+ [Theory]
+ public void Should_Get_Distance_From_CharacterHit(string text, double distance, int expectedIndex,
+ int expectedTrailingLength)
+ {
+ using (Start())
+ {
+ var glyphRun =
+ TextShaper.Current.ShapeText(text.AsMemory(), Typeface.Default, 10, CultureInfo.CurrentCulture);
+
+ var characterHit = glyphRun.GetCharacterHitFromDistance(distance, out _);
+
+ Assert.Equal(expectedIndex, characterHit.FirstCharacterIndex);
+
+ Assert.Equal(expectedTrailingLength, characterHit.TrailingLength);
+ }
+ }
+
+ private static IDisposable Start()
+ {
+ var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
+ .With(renderInterface: new PlatformRenderInterface(null),
+ textShaperImpl: new TextShaperImpl(),
+ fontManagerImpl: new CustomFontManagerImpl()));
+
+ return disposable;
+ }
+ }
+}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs
deleted file mode 100644
index 6a5065939e..0000000000
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System;
-using Avalonia.Media.TextFormatting;
-using Avalonia.Utilities;
-
-namespace Avalonia.Skia.UnitTests.Media.TextFormatting
-{
- internal class FormattableTextSource : ITextSource
- {
- private readonly ReadOnlySlice _text;
- private readonly TextRunProperties _defaultStyle;
- private ReadOnlySlice> _styleSpans;
-
- public FormattableTextSource(string text, TextRunProperties defaultStyle,
- ReadOnlySlice> styleSpans)
- {
- _text = text.AsMemory();
-
- _defaultStyle = defaultStyle;
-
- _styleSpans = styleSpans;
- }
-
- public TextRun GetTextRun(int textSourceIndex)
- {
- if (_styleSpans.IsEmpty)
- {
- return new TextEndOfParagraph();
- }
-
- var currentSpan = _styleSpans[0];
-
- _styleSpans = _styleSpans.Skip(1);
-
- return new TextCharacters(_text.AsSlice(currentSpan.Start, currentSpan.Length),
- _defaultStyle);
- }
- }
-}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs
new file mode 100644
index 0000000000..e3a2f6e766
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Utilities;
+
+namespace Avalonia.Skia.UnitTests.Media.TextFormatting
+{
+ internal readonly struct FormattedTextSource : ITextSource
+ {
+ private readonly ReadOnlySlice _text;
+ private readonly TextRunProperties _defaultProperties;
+ private readonly IReadOnlyList> _textModifier;
+
+ public FormattedTextSource(ReadOnlySlice text, TextRunProperties defaultProperties,
+ IReadOnlyList> textModifier)
+ {
+ _text = text;
+ _defaultProperties = defaultProperties;
+ _textModifier = textModifier;
+ }
+
+ public TextRun GetTextRun(int textSourceIndex)
+ {
+ if (textSourceIndex > _text.End)
+ {
+ return null;
+ }
+
+ var runText = _text.Skip(textSourceIndex);
+
+ if (runText.IsEmpty)
+ {
+ return new TextEndOfParagraph();
+ }
+
+ var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);
+
+ return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
+ }
+
+ ///
+ /// Creates a span of text run properties that has modifier applied.
+ ///
+ /// The text to create the properties for.
+ /// The default text properties.
+ /// The text properties modifier.
+ ///
+ /// The created text style run.
+ ///
+ private static ValueSpan CreateTextStyleRun(ReadOnlySlice text,
+ TextRunProperties defaultProperties, IReadOnlyList> textModifier)
+ {
+ if (textModifier == null || textModifier.Count == 0)
+ {
+ return new ValueSpan(text.Start, text.Length, defaultProperties);
+ }
+
+ var currentProperties = defaultProperties;
+
+ var hasOverride = false;
+
+ var i = 0;
+
+ var length = 0;
+
+ for (; i < textModifier.Count; i++)
+ {
+ var propertiesOverride = textModifier[i];
+
+ var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
+
+ if (textRange.End < text.Start)
+ {
+ continue;
+ }
+
+ if (textRange.Start > text.End)
+ {
+ length = text.Length;
+ break;
+ }
+
+ if (textRange.Start > text.Start)
+ {
+ if (propertiesOverride.Value != currentProperties)
+ {
+ length = Math.Min(Math.Abs(textRange.Start - text.Start), text.Length);
+
+ break;
+ }
+ }
+
+ length += Math.Min(text.Length - length, textRange.Length);
+
+ if (hasOverride)
+ {
+ continue;
+ }
+
+ hasOverride = true;
+
+ currentProperties = propertiesOverride.Value;
+ }
+
+ if (length < text.Length && i == textModifier.Count)
+ {
+ if (currentProperties == defaultProperties)
+ {
+ length = text.Length;
+ }
+ }
+
+ if (length != text.Length)
+ {
+ text = text.Take(length);
+ }
+
+ return new ValueSpan(text.Start, length, currentProperties);
+ }
+ }
+}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
index 40aa862906..2a20fdd9fe 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
@@ -20,6 +20,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public TextRun GetTextRun(int textSourceIndex)
{
+ if (textSourceIndex > 50)
+ {
+ return null;
+ }
+
if (textSourceIndex == 50)
{
return new TextEndOfParagraph();
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
index 045deacd0b..65c342b065 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
@@ -17,8 +17,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public TextRun GetTextRun(int textSourceIndex)
{
+ if (textSourceIndex > _text.Length)
+ {
+ return null;
+ }
+
var runText = _text.Skip(textSourceIndex);
-
+
if (runText.IsEmpty)
{
return new TextEndOfParagraph();
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index 7f9713930a..9c2a1953f1 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -81,7 +81,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new ValueSpan(9, 1, defaultProperties)
};
- var textSource = new FormattableTextSource(text, defaultProperties, GenericTextRunPropertiesRuns);
+ var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, GenericTextRunPropertiesRuns);
var formatter = new TextFormatterImpl();
@@ -125,6 +125,27 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
+ [Fact]
+ public void Should_Produce_A_Single_Fallback_Run()
+ {
+ using (Start())
+ {
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+
+ const string text = "👍 👍 👍 👍";
+
+ var textSource = new SingleBufferTextSource(text, defaultProperties);
+
+ var formatter = new TextFormatterImpl();
+
+ var textLine =
+ formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+ new GenericTextParagraphProperties(defaultProperties));
+
+ Assert.Equal(1, textLine.TextRuns.Count);
+ }
+ }
+
[Fact]
public void Should_Split_Run_On_Script()
{
@@ -167,7 +188,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var textLine =
formatter.FormatLine(textSource, currentPosition, 1,
- new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.WrapWithOverflow));
+ new GenericTextParagraphProperties(defaultProperties, textWrap : TextWrapping.WrapWithOverflow));
if (text.Length - currentPosition > expectedCharactersPerLine)
{
@@ -223,7 +244,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var textLine =
formatter.FormatLine(textSource, currentPosition, paragraphWidth,
- new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap));
+ new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
Assert.True(expected.Contains(textLine.TextRange.End));
@@ -256,7 +277,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties, lineHeight: 50));
- Assert.Equal(50, textLine.LineMetrics.Size.Height);
+ Assert.Equal(50, textLine.Height);
}
}
@@ -273,7 +294,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
- var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
var textSource = new SingleBufferTextSource(text, defaultProperties);
@@ -286,7 +307,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, textSourceIndex, 200, paragraphProperties);
- Assert.True(textLine.LineMetrics.Size.Width <= 200);
+ Assert.True(textLine.Width <= 200);
textSourceIndex += textLine.TextRange.Length;
}
@@ -301,7 +322,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
const string text = "012345";
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
- var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
@@ -321,6 +342,105 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
+ [InlineData("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor",
+ new []{ "Lorem ipsum ", "dolor sit amet, ", "consectetur ", "adipisicing elit, ", "sed do eiusmod "})]
+
+ [Theory]
+ public void Should_Produce_Wrapped_And_Trimmed_Lines(string text, string[] expectedLines)
+ {
+ using (Start())
+ {
+ var typeface = new Typeface("Verdana");
+
+ var defaultProperties = new GenericTextRunProperties(typeface, 32, foregroundBrush: Brushes.Black);
+
+ var styleSpans = new[]
+ {
+ new ValueSpan(0, 5,
+ new GenericTextRunProperties(typeface, 48)),
+ new ValueSpan(6, 11,
+ new GenericTextRunProperties(new Typeface("Verdana", weight: FontWeight.Bold), 32)),
+ new ValueSpan(28, 28,
+ new GenericTextRunProperties(new Typeface("Verdana", FontStyle.Italic),32))
+ };
+
+ var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, styleSpans);
+
+ var formatter = new TextFormatterImpl();
+
+ var currentPosition = 0;
+
+ var currentHeight = 0d;
+
+ var currentLineIndex = 0;
+
+ while (currentPosition < text.Length && currentLineIndex < expectedLines.Length)
+ {
+ var textLine =
+ formatter.FormatLine(textSource, currentPosition, 300,
+ new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow));
+
+ currentPosition += textLine.TextRange.Length;
+
+ if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
+ {
+ textLine = textLine.Collapse(new TextTrailingWordEllipsis(300, defaultProperties));
+ }
+
+ currentHeight += textLine.Height;
+
+ var currentText = text.Substring(textLine.TextRange.Start, textLine.TextRange.Length);
+
+ Assert.Equal(expectedLines[currentLineIndex], currentText);
+
+ currentLineIndex++;
+ }
+
+ Assert.Equal(expectedLines.Length,currentLineIndex);
+ }
+ }
+
+ [InlineData(TextAlignment.Left)]
+ [InlineData(TextAlignment.Center)]
+ [InlineData(TextAlignment.Right)]
+ [Theory]
+ public void Should_Align_TextLine(TextAlignment textAlignment)
+ {
+ using (Start())
+ {
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textAlignment);
+
+ var textSource = new SingleBufferTextSource("0123456789", defaultProperties);
+ var formatter = new TextFormatterImpl();
+
+ var textLine =
+ formatter.FormatLine(textSource, 0, 100, paragraphProperties);
+
+ var expectedOffset = TextLine.GetParagraphOffsetX(textLine.Width, 100, textAlignment);
+
+ Assert.Equal(expectedOffset, textLine.Start);
+ }
+ }
+
+ [Fact]
+ public void Should_FormatLine_With_Emergency_Breaks()
+ {
+ using (Start())
+ {
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
+
+ var textSource = new SingleBufferTextSource("0123456789_0123456789_0123456789_0123456789", defaultProperties);
+ var formatter = new TextFormatterImpl();
+
+ var textLine =
+ formatter.FormatLine(textSource, 0, 33, paragraphProperties);
+
+ Assert.NotNull(textLine.TextLineBreak?.RemainingCharacters);
+ }
+ }
+
public static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
index f7bc75c05d..afa1fbf461 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
@@ -11,8 +11,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
public class TextLayoutTests
{
- private static readonly string s_singleLineText = "0123456789";
- private static readonly string s_multiLineText = "012345678\r\r0123456789";
+ private const string SingleLineText = "0123456789";
+ private const string MultiLineText = "01 23 45 678\r\rabc def gh ij";
[InlineData("01234\r01234\r", 3)]
[InlineData("01234\r01234", 2)]
@@ -45,7 +45,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
};
var layout = new TextLayout(
- s_multiLineText,
+ MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@@ -61,7 +61,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var actual = textRun.Text.Buffer.Span.ToString();
- Assert.Equal("12", actual);
+ Assert.Equal("1 ", actual);
Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
}
@@ -74,7 +74,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
- for (var i = 4; i < s_multiLineText.Length; i++)
+ var expected = new TextLayout(
+ MultiLineText,
+ Typeface.Default,
+ 12.0f,
+ Brushes.Black.ToImmutable(),
+ textWrapping: TextWrapping.Wrap,
+ maxWidth: 25);
+
+ var expectedLines = expected.TextLines.Select(x => MultiLineText.Substring(x.TextRange.Start,
+ x.TextRange.Length)).ToList();
+
+ for (var i = 4; i < MultiLineText.Length; i++)
{
var spans = new[]
{
@@ -82,16 +93,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
};
- var expected = new TextLayout(
- s_multiLineText,
- Typeface.Default,
- 12.0f,
- Brushes.Black.ToImmutable(),
- textWrapping: TextWrapping.Wrap,
- maxWidth: 25);
-
var actual = new TextLayout(
- s_multiLineText,
+ MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@@ -99,14 +102,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
maxWidth: 25,
textStyleOverrides: spans);
- Assert.Equal(expected.TextLines.Count, actual.TextLines.Count);
+ var actualLines = actual.TextLines.Select(x => MultiLineText.Substring(x.TextRange.Start,
+ x.TextRange.Length)).ToList();
+
+ Assert.Equal(expectedLines.Count, actualLines.Count);
for (var j = 0; j < actual.TextLines.Count; j++)
{
- Assert.Equal(expected.TextLines[j].TextRange.Length, actual.TextLines[j].TextRange.Length);
+ var expectedText = expectedLines[j];
+
+ var actualText = actualLines[j];
- Assert.Equal(expected.TextLines[j].TextRuns.Sum(x => x.Text.Length),
- actual.TextLines[j].TextRuns.Sum(x => x.Text.Length));
+ Assert.Equal(expectedText, actualText);
}
}
}
@@ -126,7 +133,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
};
var layout = new TextLayout(
- s_singleLineText,
+ SingleLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@@ -140,7 +147,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(2, textRun.Text.Length);
- var actual = s_singleLineText.Substring(textRun.Text.Start,
+ var actual = SingleLineText.Substring(textRun.Text.Start,
textRun.Text.Length);
Assert.Equal("01", actual);
@@ -163,7 +170,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
};
var layout = new TextLayout(
- s_singleLineText,
+ SingleLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@@ -261,12 +268,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var layout = new TextLayout(
- s_multiLineText,
+ MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable());
- Assert.Equal(s_multiLineText.Length, layout.TextLines.Sum(x => x.TextRange.Length));
+ Assert.Equal(MultiLineText.Length, layout.TextLines.Sum(x => x.TextRange.Length));
}
}
@@ -276,13 +283,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var layout = new TextLayout(
- s_multiLineText,
+ MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable());
Assert.Equal(
- s_multiLineText.Length,
+ MultiLineText.Length,
layout.TextLines.Select(textLine =>
textLine.TextRuns.Sum(textRun => textRun.Text.Length))
.Sum());
@@ -295,9 +302,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
const string text =
- "Multiline TextBox with TextWrapping.\r\rLorem ipsum dolor sit amet, consectetur adipiscing elit. " +
- "Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. " +
- "Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.";
+ "Multiline TextBox with TextWrapping.\r\rLorem ipsum dolor sit amet";
var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
@@ -338,7 +343,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
};
var layout = new TextLayout(
- s_multiLineText,
+ MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@@ -541,7 +546,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var layout = new TextLayout(
- s_multiLineText,
+ MultiLineText,
Typeface.Default,
12,
Brushes.Black,
@@ -549,7 +554,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
foreach (var line in layout.TextLines)
{
- Assert.Equal(50, line.LineMetrics.Size.Height);
+ Assert.Equal(50, line.Height);
}
}
}
@@ -601,7 +606,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var layout = new TextLayout(
- s_singleLineText,
+ SingleLineText,
Typeface.Default,
12,
Brushes.Black,
@@ -609,7 +614,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
maxWidth: 3);
//every character should be new line as there not enough space for even one character
- Assert.Equal(s_singleLineText.Length, layout.TextLines.Count);
+ Assert.Equal(SingleLineText.Length, layout.TextLines.Count);
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index 8f1bd5979a..5961806c5c 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -71,6 +71,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
[InlineData("𐐷𐐷𐐷𐐷𐐷")]
+ [InlineData("01234567🎉\n")]
[InlineData("𐐷1234")]
[Theory]
public void Should_Get_Next_Caret_CharacterHit(string text)
@@ -109,9 +110,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
- for (var i = 0; i < clusters.Length; i++)
+ foreach (var cluster in clusters)
{
- Assert.Equal(clusters[i], nextCharacterHit.FirstCharacterIndex);
+ Assert.Equal(cluster, nextCharacterHit.FirstCharacterIndex);
nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit);
}
@@ -127,6 +128,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
[InlineData("𐐷𐐷𐐷𐐷𐐷")]
+ [InlineData("01234567🎉\n")]
[InlineData("𐐷1234")]
[Theory]
public void Should_Get_Previous_Caret_CharacterHit(string text)
@@ -269,14 +271,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
- characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width);
+ characterHit = textLine.GetCharacterHitFromDistance(textLine.Width);
Assert.Equal(MultiBufferTextSource.TextRange.End, characterHit.FirstCharacterIndex);
}
}
[InlineData("01234 01234", 8, TextCollapsingStyle.TrailingCharacter, "01234 0\u2026")]
- [InlineData("01234 01234", 8, TextCollapsingStyle.TrailingWord, "01234 \u2026")]
+ [InlineData("01234 01234", 8, TextCollapsingStyle.TrailingWord, "01234\u2026")]
[Theory]
public void Should_Collapse_Line(string text, int numberOfCharacters, TextCollapsingStyle style, string expected)
{
@@ -333,8 +335,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
- [Fact]
- public void Should_Ignore_Invisible_Characters()
+ [Fact(Skip = "Verify this")]
+ public void Should_Ignore_NewLine_Characters()
{
using (Start())
{
@@ -356,6 +358,28 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(new CharacterHit(8, 2), nextCharacterHit);
}
}
+
+ [Fact]
+ public void TextLineBreak_Should_Contain_TextEndOfLine()
+ {
+ using (Start())
+ {
+ var defaultTextRunProperties =
+ new GenericTextRunProperties(Typeface.Default);
+
+ const string text = "0123456789";
+
+ var source = new SingleBufferTextSource(text, defaultTextRunProperties);
+
+ var textParagraphProperties = new GenericTextParagraphProperties(defaultTextRunProperties);
+
+ var formatter = TextFormatter.Current;
+
+ var textLine = formatter.FormatLine(source, 0, double.PositiveInfinity, textParagraphProperties);
+
+ Assert.NotNull(textLine.TextLineBreak.TextEndOfLine);
+ }
+ }
private static IDisposable Start()
{
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
new file mode 100644
index 0000000000..62d2c54ffe
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Globalization;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Skia.UnitTests.Media.TextFormatting
+{
+ public class TextShaperTests
+ {
+ [Fact]
+ public void Should_Form_Clusters_For_BreakPairs()
+ {
+ using (Start())
+ {
+ var text = "\n\r\n".AsMemory();
+
+ var glyphRun = TextShaper.Current.ShapeText(
+ text,
+ Typeface.Default,
+ 12,
+ CultureInfo.CurrentCulture);
+
+ Assert.Equal(glyphRun.Characters.Length, text.Length);
+ Assert.Equal(glyphRun.GlyphClusters.Length, text.Length);
+ Assert.Equal(0, glyphRun.GlyphClusters[0]);
+ Assert.Equal(1, glyphRun.GlyphClusters[1]);
+ Assert.Equal(1, glyphRun.GlyphClusters[2]);
+ }
+ }
+
+ private static IDisposable Start()
+ {
+ var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
+ .With(renderInterface: new PlatformRenderInterface(null),
+ textShaperImpl: new TextShaperImpl(),
+ fontManagerImpl: new CustomFontManagerImpl()));
+
+ return disposable;
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
index e73a76357a..74366f9e26 100644
--- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
+++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
@@ -66,6 +66,28 @@ namespace Avalonia.UnitTests
return Mock.Of();
}
+ public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(string fileName)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream)
+ {
+ throw new NotImplementedException();
+ }
+
public IBitmapImpl LoadBitmap(string fileName)
{
return Mock.Of();
@@ -97,9 +119,8 @@ namespace Avalonia.UnitTests
throw new NotImplementedException();
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
- width = 0;
return Mock.Of();
}
diff --git a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
index 4fa3fbf523..864e2efbaf 100644
--- a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
+++ b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
@@ -30,6 +30,8 @@ namespace Avalonia.UnitTests
public IGeometryImpl SourceGeometry { get; }
public Rect Bounds => _context.CalculateBounds();
+
+ public double ContourLength { get; }
public Matrix Transform { get; }
@@ -69,6 +71,25 @@ namespace Avalonia.UnitTests
return new MockStreamGeometryImpl(transform, _context);
}
+ public bool TryGetPointAtDistance(double distance, out Point point)
+ {
+ point = new Point();
+ return false;
+ }
+
+ public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent)
+ {
+ point = new Point();
+ tangent = new Point();
+ return false;
+ }
+
+ public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry)
+ {
+ segmentGeometry = null;
+ return false;
+ }
+
class MockStreamGeometryContext : IStreamGeometryContextImpl
{
private List points = new List();
diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/CustomHitTestBorder.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/CustomHitTestBorder.cs
new file mode 100644
index 0000000000..3221f097ad
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Rendering/CustomHitTestBorder.cs
@@ -0,0 +1,17 @@
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Rendering;
+
+namespace Avalonia.Visuals.UnitTests.Rendering
+{
+ internal class CustomHitTestBorder : Border, ICustomHitTest
+ {
+ public bool HitTest(Point point)
+ {
+ // Move hit testing window halfway to the left
+ return Bounds
+ .WithX(Bounds.X - Bounds.Width / 2)
+ .Contains(point);
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs
index 90b01e2ddb..d24d183709 100644
--- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs
@@ -1,6 +1,9 @@
-using System.Linq;
+using System;
+using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Shapes;
+using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
@@ -9,8 +12,6 @@ using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
-using System;
-using Avalonia.Controls.Shapes;
namespace Avalonia.Visuals.UnitTests.Rendering
{
@@ -501,6 +502,42 @@ namespace Avalonia.Visuals.UnitTests.Rendering
}
}
+ [Fact]
+ public void HitTest_Should_Accommodate_ICustomHitTest()
+ {
+ using (TestApplication())
+ {
+ Border border;
+
+ var root = new TestRoot
+ {
+ Width = 300,
+ Height = 200,
+ Child = border = new CustomHitTestBorder
+ {
+ Width = 100,
+ Height = 100,
+ Background = Brushes.Red,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ }
+ };
+
+ root.Renderer = new DeferredRenderer(root, null);
+ root.Measure(Size.Infinity);
+ root.Arrange(new Rect(root.DesiredSize));
+
+ var result = root.Renderer.HitTest(new Point(75, 100), root, null);
+ Assert.Equal(new[] { border }, result);
+
+ result = root.Renderer.HitTest(new Point(125, 100), root, null);
+ Assert.Equal(new[] { border }, result);
+
+ result = root.Renderer.HitTest(new Point(175, 100), root, null);
+ Assert.Empty(result);
+ }
+ }
+
private IDisposable TestApplication()
{
return UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs
index c4ef9d5ee7..a3b0a0cdd5 100644
--- a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs
@@ -413,6 +413,43 @@ namespace Avalonia.Visuals.UnitTests.Rendering
}
}
+ [Fact]
+ public void HitTest_Should_Accommodate_ICustomHitTest()
+ {
+ using (TestApplication())
+ {
+ Border border;
+
+ var root = new TestRoot
+ {
+ Width = 300,
+ Height = 200,
+ Child = border = new CustomHitTestBorder
+ {
+ Width = 100,
+ Height = 100,
+ Background = Brushes.Red,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ }
+ };
+
+ root.Renderer = new ImmediateRenderer(root);
+ root.Measure(Size.Infinity);
+ root.Arrange(new Rect(root.DesiredSize));
+ root.Renderer.Paint(new Rect(root.ClientSize));
+
+ var result = root.Renderer.HitTest(new Point(75, 100), root, null).First();
+ Assert.Equal(border, result);
+
+ result = root.Renderer.HitTest(new Point(125, 100), root, null).First();
+ Assert.Equal(border, result);
+
+ result = root.Renderer.HitTest(new Point(175, 100), root, null).First();
+ Assert.Equal(root, result);
+ }
+ }
+
private IDisposable TestApplication()
{
return UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
diff --git a/tests/Avalonia.Visuals.UnitTests/VectorTests.cs b/tests/Avalonia.Visuals.UnitTests/VectorTests.cs
index 0e72cd7c7f..f9a9e59436 100644
--- a/tests/Avalonia.Visuals.UnitTests/VectorTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VectorTests.cs
@@ -105,5 +105,15 @@ namespace Avalonia.Visuals.UnitTests
Assert.Equal(expected, Vector.Multiply(vector, 2));
}
+
+ [Fact]
+ public void Scale_Vector_Should_Be_Commutative()
+ {
+ var vector = new Vector(10, 2);
+
+ var expected = vector * 2;
+
+ Assert.Equal(expected, 2 * vector);
+ }
}
}
diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
index 02236e4107..51ea1e893f 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
@@ -42,6 +42,28 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
+ public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(string fileName)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream)
+ {
+ throw new NotImplementedException();
+ }
+
public IBitmapImpl LoadBitmap(string fileName)
{
throw new NotImplementedException();
@@ -52,7 +74,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
throw new NotImplementedException();
}
@@ -112,6 +134,8 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
}
}
+ public double ContourLength { get; }
+
public IStreamGeometryImpl Clone()
{
return this;
@@ -151,6 +175,21 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
+ public bool TryGetPointAtDistance(double distance, out Point point)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry)
+ {
+ throw new NotImplementedException();
+ }
+
class MockStreamGeometryContext : IStreamGeometryContextImpl
{
private List points = new List();
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png
index ea81296017..110b44a4c0 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
index a8ec09df99..a74af7a06e 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png
index 485b03acd3..7e4faf77d5 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png
index a6bc0daee3..3b91d04f7c 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png
index 8c622c37b5..1df122151d 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png
index bfe5fd7fbb..aedc6afe30 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png
index f3c3c4aee2..ccdcab84e4 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png
index 80116d81c6..2d7ccac2f7 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png
index f366912df5..d9c62a72a8 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png
index 474e2bb2bf..97ca065be8 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
index 4ca25db112..e3e6297c96 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png
index 3c9c88e8f1..c0565f8963 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png
index e9efe1e796..cdf11c52d6 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png
index b864ffca33..ae2f2a4c21 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png
index fa359eac11..c8cdded708 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png
index 778e3f6c13..3cde267980 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png
index d7ef920967..6468f46b51 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png
index 152c703f93..901b44a0ed 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png differ