diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 34f931ff41..46e8665945 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -18,7 +18,7 @@
- [ ] 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
diff --git a/Documentation/build.md b/Documentation/build.md
index 5f75290424..a7d68eb599 100644
--- a/Documentation/build.md
+++ b/Documentation/build.md
@@ -9,10 +9,24 @@ git clone https://github.com/AvaloniaUI/Avalonia.git
git submodule update --init
```
+### Install the required version of the .NET Core SDK
+
+Go to https://dotnet.microsoft.com/download/visual-studio-sdks and install the latest version of the .NET Core SDK compatible with Avalonia UI. Make sure to download the SDK (not just the "runtime") package. The version compatible is indicated within the [global.json](https://github.com/AvaloniaUI/Avalonia/blob/master/global.json) file. Note that Avalonia UI does not always use the latest version and is hardcoded to use the last version known to be compatible (SDK releases may break the builds from time-to-time).
+
### Open in Visual Studio
-Open the `Avalonia.sln` solution in Visual Studio 2019 or newer. The free Visual Studio Community
-edition works fine. Run the `Samples\ControlCatalog.Desktop` project to see the sample application.
+Open the `Avalonia.sln` solution in Visual Studio 2019 or newer. The free Visual Studio Community edition works fine. Build and run the `Samples\ControlCatalog.Desktop` or `ControlCatalog.NetCore` project to see the sample application.
+
+### Troubleshooting
+
+ * **Error CS0006: Avalonia.DesktopRuntime.dll could not be found**
+
+ It is common for the first build to fail with the errors below (also discussed in [#4257](https://github.com/AvaloniaUI/Avalonia/issues/4257)).
+ ```
+ >CSC : error CS0006: Metadata file 'C:\...\Avalonia\src\Avalonia.DesktopRuntime\bin\Debug\netcoreapp2.0\Avalonia.DesktopRuntime.dll' could not be found
+ >CSC : error CS0006: Metadata file 'C:\...\Avalonia\packages\Avalonia\bin\Debug\netcoreapp2.0\Avalonia.dll' could not be found
+ ```
+ To correct this, right click on the `Avalonia.DesktopRuntime` project then press `Build` to build the project manually. Afterwards the solution should build normally and the ControlCatalog can be run.
# Linux/macOS
@@ -20,9 +34,9 @@ It's *not* possible to build the *whole* project on Linux/macOS. You can only bu
MonoDevelop, Xamarin Studio and Visual Studio for Mac aren't capable of properly opening our solution. You can use Rider (at least 2017.2 EAP) or VSCode instead. They will fail to load most of platform specific projects, but you don't need them to run on .NET Core.
-### Install the latest version of .NET Core
+### Install the latest version of the .NET Core SDK
-Go to https://www.microsoft.com/net/core and follow instructions for your OS. You need SDK (not just "runtime") package.
+Go to https://www.microsoft.com/net/core and follow the instructions for your OS. Make sure to download the SDK (not just the "runtime") package.
### Additional requirements for macOS
diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index d7afbdaa3a..c0936356d2 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/native/Avalonia.Native/src/OSX/window.mm
@@ -50,7 +50,6 @@ public:
[Window setBackingType:NSBackingStoreBuffered];
[Window setOpaque:false];
- [Window setContentView: StandardContainer];
}
virtual HRESULT ObtainNSWindowHandle(void** ret) override
@@ -112,6 +111,9 @@ public:
{
SetPosition(lastPositionSet);
UpdateStyle();
+
+ [Window setContentView: StandardContainer];
+
if(ShouldTakeFocusOnShow() && activate)
{
[Window makeKeyAndOrderFront:Window];
@@ -124,7 +126,7 @@ public:
[Window setTitle:_lastTitle];
_shown = true;
-
+
return S_OK;
}
}
@@ -191,9 +193,11 @@ public:
{
if(ret == nullptr)
return E_POINTER;
+
auto frame = [View frame];
ret->Width = frame.size.width;
ret->Height = frame.size.height;
+
return S_OK;
}
}
@@ -254,6 +258,12 @@ public:
y = maxSize.height;
}
+ if(!_shown)
+ {
+ BaseEvents->Resized(AvnSize{x,y});
+ }
+
+ [StandardContainer setFrameSize:NSSize{x,y}];
[Window setContentSize:NSSize{x, y}];
return S_OK;
@@ -503,6 +513,7 @@ private:
bool _fullScreenActive;
SystemDecorations _decorations;
AvnWindowState _lastWindowState;
+ AvnWindowState _actualWindowState;
bool _inSetWindowState;
NSRect _preZoomSize;
bool _transitioningWindowState;
@@ -529,6 +540,7 @@ private:
_transitioningWindowState = false;
_inSetWindowState = false;
_lastWindowState = Normal;
+ _actualWindowState = Normal;
WindowEvents = events;
[Window setCanBecomeKeyAndMain];
[Window disableCursorRects];
@@ -623,7 +635,7 @@ private:
void WindowStateChanged () override
{
- if(!_inSetWindowState && !_transitioningWindowState)
+ if(_shown && !_inSetWindowState && !_transitioningWindowState)
{
AvnWindowState state;
GetWindowState(&state);
@@ -953,14 +965,14 @@ private:
{
@autoreleasepool
{
- if(_lastWindowState == state)
+ if(_actualWindowState == state)
{
return S_OK;
}
_inSetWindowState = true;
- auto currentState = _lastWindowState;
+ auto currentState = _actualWindowState;
_lastWindowState = state;
if(currentState == Normal)
@@ -1039,8 +1051,11 @@ private:
}
break;
}
+
+ _actualWindowState = _lastWindowState;
}
+
_inSetWindowState = false;
return S_OK;
@@ -1996,7 +2011,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_lastScaling = [self backingScaleFactor];
[self setOpaque:NO];
[self setBackgroundColor: [NSColor clearColor]];
- [self invalidateShadow];
_isExtended = false;
return self;
}
@@ -2237,6 +2251,7 @@ protected:
{
if (Window != nullptr)
{
+ [StandardContainer setFrameSize:NSSize{x,y}];
[Window setContentSize:NSSize{x, y}];
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];
diff --git a/readme.md b/readme.md
index 67b706f428..7959bc5540 100644
--- a/readme.md
+++ b/readme.md
@@ -14,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/
@@ -52,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/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/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs
index 723351ae57..2446c0e1c9 100644
--- a/samples/ControlCatalog/MainWindow.xaml.cs
+++ b/samples/ControlCatalog/MainWindow.xaml.cs
@@ -17,7 +17,10 @@ namespace ControlCatalog
public MainWindow()
{
this.InitializeComponent();
- this.AttachDevTools();
+ this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
+ {
+ StartupScreenIndex = 1,
+ });
//Renderer.DrawFps = true;
//Renderer.DrawDirtyRects = Renderer.DrawFps = true;
diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
index e15637aa0f..f0e079ad91 100644
--- a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
+++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
@@ -84,13 +84,13 @@
-
+
-
+
-
+
diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml b/samples/ControlCatalog/Pages/ViewboxPage.xaml
index e78cf2bc22..ef802db33e 100644
--- a/samples/ControlCatalog/Pages/ViewboxPage.xaml
+++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml
@@ -1,66 +1,36 @@
-
-
- F1 M 16.6309,18.6563C 17.1309,
- 8.15625 29.8809,14.1563 29.8809,
- 14.1563C 30.8809,11.1563 34.1308,
- 11.4063 34.1308,11.4063C 33.5,12
- 34.6309,13.1563 34.6309,13.1563C
- 32.1309,13.1562 31.1309,14.9062
- 31.1309,14.9062C 41.1309,23.9062
- 32.6309,27.9063 32.6309,27.9062C
- 24.6309,24.9063 21.1309,22.1562
- 16.6309,18.6563 Z M 16.6309,19.9063C
- 21.6309,24.1563 25.1309,26.1562
- 31.6309,28.6562C 31.6309,28.6562
- 26.3809,39.1562 18.3809,36.1563C
- 18.3809,36.1563 18,38 16.3809,36.9063C
- 15,36 16.3809,34.9063 16.3809,34.9063C
- 16.3809,34.9063 10.1309,30.9062 16.6309,19.9063 Z
-
-
-
+
Viewbox
A control used to scale single child.
-
- None
- Fill
- Uniform
- UniformToFill
-
- Hello World!
-
-
- Hello World!
-
-
- Hello World!
-
-
- Hello World!
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs b/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
index 1b5f4bc7f4..94b3f3ea14 100644
--- a/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
@@ -1,5 +1,6 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using Avalonia.Media;
namespace ControlCatalog.Pages
{
@@ -7,7 +8,25 @@ namespace ControlCatalog.Pages
{
public ViewboxPage()
{
- this.InitializeComponent();
+ InitializeComponent();
+
+ var stretchSelector = this.FindControl("StretchSelector");
+
+ stretchSelector.Items = new[]
+ {
+ Stretch.Uniform, Stretch.UniformToFill, Stretch.Fill, Stretch.None
+ };
+
+ stretchSelector.SelectedIndex = 0;
+
+ var stretchDirectionSelector = this.FindControl("StretchDirectionSelector");
+
+ stretchDirectionSelector.Items = new[]
+ {
+ StretchDirection.Both, StretchDirection.DownOnly, StretchDirection.UpOnly
+ };
+
+ stretchDirectionSelector.SelectedIndex = 0;
}
private void InitializeComponent()
diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index aa165d13f7..f37df56b73 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -36,6 +36,9 @@
+
+
+
diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml
index 12fb31ea59..21c7d68b5d 100644
--- a/samples/RenderDemo/Pages/AnimationsPage.xaml
+++ b/samples/RenderDemo/Pages/AnimationsPage.xaml
@@ -1,7 +1,8 @@
+ x:Class="RenderDemo.Pages.AnimationsPage"
+ MaxWidth="600">
@@ -167,8 +168,8 @@
-
- Hover to activate Transform Keyframe Animations.
+
+ Hover to activate Keyframe Animations.
diff --git a/samples/RenderDemo/Pages/CustomAnimatorPage.xaml b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml
new file mode 100644
index 0000000000..b386636cae
--- /dev/null
+++ b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs
new file mode 100644
index 0000000000..eed8ee29ce
--- /dev/null
+++ b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs
@@ -0,0 +1,27 @@
+using System.Reactive.Linq;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using RenderDemo.ViewModels;
+
+namespace RenderDemo.Pages
+{
+ public class CustomAnimatorPage : UserControl
+ {
+ public CustomAnimatorPage()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/RenderDemo/Pages/CustomStringAnimator.cs b/samples/RenderDemo/Pages/CustomStringAnimator.cs
new file mode 100644
index 0000000000..851a2d0187
--- /dev/null
+++ b/samples/RenderDemo/Pages/CustomStringAnimator.cs
@@ -0,0 +1,16 @@
+using Avalonia.Animation.Animators;
+
+namespace RenderDemo.Pages
+{
+ public class CustomStringAnimator : Animator
+ {
+ public override string Interpolate(double progress, string oldValue, string newValue)
+ {
+ if (newValue.Length == 0) return "";
+ var step = 1.0 / newValue.Length;
+ var length = (int)(progress / step);
+ var result = newValue.Substring(0, length + 1);
+ return result;
+ }
+ }
+}
diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml
index d6da293ff3..1985074b0f 100644
--- a/samples/RenderDemo/Pages/TransitionsPage.xaml
+++ b/samples/RenderDemo/Pages/TransitionsPage.xaml
@@ -1,7 +1,8 @@
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ x:Class="RenderDemo.Pages.TransitionsPage"
+ MaxWidth="600">
@@ -90,6 +91,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -98,8 +182,8 @@
-
- Hover to activate Transform Keyframe Animations.
+
+ Hover to activate Transitions.
@@ -109,6 +193,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs
index 05142532e9..172782c5a9 100644
--- a/src/Avalonia.Animation/Animation.cs
+++ b/src/Avalonia.Animation/Animation.cs
@@ -3,10 +3,11 @@ using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
+using System.Threading;
using System.Threading.Tasks;
+
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
-using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Metadata;
@@ -194,6 +195,33 @@ namespace Avalonia.Animation
[Content]
public KeyFrames Children { get; } = new KeyFrames();
+ // Store values for the Animator attached properties for IAnimationSetter objects.
+ private static readonly Dictionary s_animators = new Dictionary();
+
+ ///
+ /// Gets the value of the Animator attached property for a setter.
+ ///
+ /// The animation setter.
+ /// The property animator type.
+ public static Type GetAnimator(IAnimationSetter setter)
+ {
+ if (s_animators.TryGetValue(setter, out var type))
+ {
+ return type;
+ }
+ return null;
+ }
+
+ ///
+ /// Sets the value of the Animator attached property for a setter.
+ ///
+ /// The animation setter.
+ /// The property animator value.
+ public static void SetAnimator(IAnimationSetter setter, Type value)
+ {
+ s_animators[setter] = value;
+ }
+
private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)>
{
( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator) ),
@@ -209,6 +237,17 @@ namespace Avalonia.Animation
( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator) ),
};
+ ///
+ /// Registers a that can handle
+ /// a value type that matches the specified condition.
+ ///
+ ///
+ /// The condition to which the
+ /// is to be activated and used.
+ ///
+ ///
+ /// The type of the animator to instantiate.
+ ///
public static void RegisterAnimator(Func condition)
where TAnimator : IAnimator
{
@@ -237,7 +276,7 @@ namespace Avalonia.Animation
{
foreach (var setter in keyframe.Setters)
{
- var handler = GetAnimatorType(setter.Property);
+ var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property);
if (handler == null)
{
@@ -281,7 +320,7 @@ namespace Avalonia.Animation
return (newAnimatorInstances, subscriptions);
}
- ///
+ ///
public IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete)
{
var (animators, subscriptions) = InterpretKeyframes(control);
@@ -306,25 +345,40 @@ namespace Avalonia.Animation
if (onComplete != null)
{
- Task.WhenAll(completionTasks).ContinueWith(_ => onComplete());
+ Task.WhenAll(completionTasks).ContinueWith(
+ (_, state) => ((Action)state).Invoke(),
+ onComplete);
}
}
return new CompositeDisposable(subscriptions);
}
- ///
- public Task RunAsync(Animatable control, IClock clock = null)
+ ///
+ public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default)
{
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return Task.CompletedTask;
+ }
+
var run = new TaskCompletionSource();
if (this.IterationCount == IterationCount.Infinite)
run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
- IDisposable subscriptions = null;
+ IDisposable subscriptions = null, cancellation = null;
subscriptions = this.Apply(control, clock, Observable.Return(true), () =>
{
- run.SetResult(null);
+ run.TrySetResult(null);
+ subscriptions?.Dispose();
+ cancellation?.Dispose();
+ });
+
+ cancellation = cancellationToken.Register(() =>
+ {
+ run.TrySetResult(null);
subscriptions?.Dispose();
+ cancellation?.Dispose();
});
return run.Task;
diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs
index 6f601a3e13..cf79640150 100644
--- a/src/Avalonia.Animation/AnimationInstance`1.cs
+++ b/src/Avalonia.Animation/AnimationInstance`1.cs
@@ -5,6 +5,7 @@ using Avalonia.Animation.Animators;
using Avalonia.Animation.Utils;
using Avalonia.Data;
using Avalonia.Reactive;
+using JetBrains.Annotations;
namespace Avalonia.Animation
{
@@ -36,6 +37,7 @@ namespace Avalonia.Animation
private IDisposable _timerSub;
private readonly IClock _baseClock;
private IClock _clock;
+ private EventHandler _propertyChangedDelegate;
public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action OnComplete, Func Interpolator)
{
@@ -45,8 +47,6 @@ namespace Avalonia.Animation
_onCompleteAction = OnComplete;
_interpolator = Interpolator;
_baseClock = baseClock;
- _neutralValue = (T)_targetControl.GetValue(_animator.Property);
-
FetchProperties();
}
@@ -80,6 +80,7 @@ namespace Avalonia.Animation
// Animation may have been stopped before it has finished.
ApplyFinalFill();
+ _targetControl.PropertyChanged -= _propertyChangedDelegate;
_timerSub?.Dispose();
_clock.PlayState = PlayState.Stop;
}
@@ -88,6 +89,9 @@ namespace Avalonia.Animation
{
_clock = new Clock(_baseClock);
_timerSub = _clock.Subscribe(Step);
+ _propertyChangedDelegate ??= ControlPropertyChanged;
+ _targetControl.PropertyChanged += _propertyChangedDelegate;
+ UpdateNeutralValue();
}
public void Step(TimeSpan frameTick)
@@ -216,5 +220,22 @@ namespace Avalonia.Animation
}
}
}
+
+ private void UpdateNeutralValue()
+ {
+ var property = _animator.Property;
+ var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue);
+
+ _neutralValue = baseValue != AvaloniaProperty.UnsetValue ?
+ (T)baseValue : (T)_targetControl.GetValue(property);
+ }
+
+ private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == _animator.Property && e.Priority > BindingPriority.Animation)
+ {
+ UpdateNeutralValue();
+ }
+ }
}
}
diff --git a/src/Avalonia.Animation/AnimatorDrivenTransition.cs b/src/Avalonia.Animation/AnimatorDrivenTransition.cs
new file mode 100644
index 0000000000..88c8ec5ec1
--- /dev/null
+++ b/src/Avalonia.Animation/AnimatorDrivenTransition.cs
@@ -0,0 +1,20 @@
+using System;
+using Avalonia.Animation.Animators;
+
+namespace Avalonia.Animation
+{
+ ///
+ /// using an to transition between values.
+ ///
+ /// Type of the transitioned value.
+ /// Type of the animator.
+ public abstract class AnimatorDrivenTransition : Transition where TAnimator : Animator, new()
+ {
+ private static readonly TAnimator s_animator = new TAnimator();
+
+ public override IObservable DoTransition(IObservable progress, T oldValue, T newValue)
+ {
+ return new AnimatorTransitionObservable(s_animator, progress, Easing, oldValue, newValue);
+ }
+ }
+}
diff --git a/src/Avalonia.Animation/AnimatorTransitionObservable.cs b/src/Avalonia.Animation/AnimatorTransitionObservable.cs
new file mode 100644
index 0000000000..3cc185179b
--- /dev/null
+++ b/src/Avalonia.Animation/AnimatorTransitionObservable.cs
@@ -0,0 +1,32 @@
+using System;
+using Avalonia.Animation.Animators;
+using Avalonia.Animation.Easings;
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Transition observable based on an producing a value.
+ ///
+ /// Type of the transitioned value.
+ /// Type of the animator.
+ public class AnimatorTransitionObservable : TransitionObservableBase where TAnimator : Animator
+ {
+ private readonly TAnimator _animator;
+ private readonly Easing _easing;
+ private readonly T _oldValue;
+ private readonly T _newValue;
+
+ public AnimatorTransitionObservable(TAnimator animator, IObservable progress, Easing easing, T oldValue, T newValue) : base(progress, easing)
+ {
+ _animator = animator;
+ _easing = easing;
+ _oldValue = oldValue;
+ _newValue = newValue;
+ }
+
+ protected override T ProduceValue(double progress)
+ {
+ return _animator.Interpolate(progress, _oldValue, _newValue);
+ }
+ }
+}
diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs
index 0660440e30..d784227620 100644
--- a/src/Avalonia.Animation/Animators/Animator`1.cs
+++ b/src/Avalonia.Animation/Animators/Animator`1.cs
@@ -104,6 +104,11 @@ namespace Avalonia.Animation.Animators
throw new Exception("Index time is out of keyframe time range.");
}
+ public virtual IDisposable BindAnimation(Animatable control, IObservable instance)
+ {
+ return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation);
+ }
+
///
/// Runs the KeyFrames Animation.
///
@@ -116,7 +121,8 @@ namespace Avalonia.Animation.Animators
clock ?? control.Clock ?? Clock.GlobalClock,
onComplete,
InterpolationHandler);
- return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation);
+
+ return BindAnimation(control, instance);
}
///
diff --git a/src/Avalonia.Animation/ApiCompatBaseline.txt b/src/Avalonia.Animation/ApiCompatBaseline.txt
new file mode 100644
index 0000000000..58cb7830e7
--- /dev/null
+++ b/src/Avalonia.Animation/ApiCompatBaseline.txt
@@ -0,0 +1,6 @@
+Compat issues with assembly Avalonia.Animation:
+MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.Animation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock, System.Threading.CancellationToken)' is present in the implementation but not in the contract.
+Total Issues: 4
diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs
index bea6c75982..5c2b7ce0dd 100644
--- a/src/Avalonia.Animation/Clock.cs
+++ b/src/Avalonia.Animation/Clock.cs
@@ -1,8 +1,4 @@
using System;
-using System.Collections.Generic;
-using System.Reactive.Linq;
-using System.Text;
-using Avalonia.Reactive;
namespace Avalonia.Animation
{
@@ -10,10 +6,9 @@ namespace Avalonia.Animation
{
public static IClock GlobalClock => AvaloniaLocator.Current.GetService();
- private IDisposable _parentSubscription;
+ private readonly IDisposable _parentSubscription;
- public Clock()
- :this(GlobalClock)
+ public Clock() : this(GlobalClock)
{
}
diff --git a/src/Avalonia.Animation/ClockBase.cs b/src/Avalonia.Animation/ClockBase.cs
index a2b29e728e..c6e5a363be 100644
--- a/src/Avalonia.Animation/ClockBase.cs
+++ b/src/Avalonia.Animation/ClockBase.cs
@@ -1,16 +1,11 @@
using System;
-using System.Collections.Generic;
-using System.Reactive.Linq;
-using System.Text;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
public class ClockBase : IClock
{
- private ClockObservable _observable;
-
- private IObservable _connectedObservable;
+ private readonly ClockObservable _observable;
private TimeSpan? _previousTime;
private TimeSpan _internalTime;
@@ -18,7 +13,6 @@ namespace Avalonia.Animation
protected ClockBase()
{
_observable = new ClockObservable();
- _connectedObservable = _observable.Publish().RefCount();
}
protected bool HasSubscriptions => _observable.HasSubscriptions;
@@ -58,10 +52,10 @@ namespace Avalonia.Animation
public IDisposable Subscribe(IObserver observer)
{
- return _connectedObservable.Subscribe(observer);
+ return _observable.Subscribe(observer);
}
- private class ClockObservable : LightweightObservableBase
+ private sealed class ClockObservable : LightweightObservableBase
{
public bool HasSubscriptions { get; private set; }
public void Pulse(TimeSpan time) => PublishNext(time);
diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs
index ff85535d8a..d037834630 100644
--- a/src/Avalonia.Animation/IAnimation.cs
+++ b/src/Avalonia.Animation/IAnimation.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Animation
@@ -16,6 +17,6 @@ namespace Avalonia.Animation
///
/// Run the animation on the specified control.
///
- Task RunAsync(Animatable control, IClock clock);
+ Task RunAsync(Animatable control, IClock clock, CancellationToken cancellationToken = default);
}
}
diff --git a/src/Avalonia.Animation/Transition`1.cs b/src/Avalonia.Animation/Transition.cs
similarity index 96%
rename from src/Avalonia.Animation/Transition`1.cs
rename to src/Avalonia.Animation/Transition.cs
index 4542a137e5..4115c95c0f 100644
--- a/src/Avalonia.Animation/Transition`1.cs
+++ b/src/Avalonia.Animation/Transition.cs
@@ -1,7 +1,5 @@
-using System;
-using System.Reactive.Linq;
+using System;
using Avalonia.Animation.Easings;
-using Avalonia.Animation.Utils;
namespace Avalonia.Animation
{
@@ -56,4 +54,4 @@ namespace Avalonia.Animation
return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs
index 5184341324..b522d1961e 100644
--- a/src/Avalonia.Animation/TransitionInstance.cs
+++ b/src/Avalonia.Animation/TransitionInstance.cs
@@ -1,8 +1,5 @@
-using Avalonia.Metadata;
using System;
-using System.Reactive.Linq;
-using Avalonia.Animation.Easings;
-using Avalonia.Animation.Utils;
+using System.Runtime.ExceptionServices;
using Avalonia.Reactive;
using Avalonia.Utilities;
@@ -11,13 +8,13 @@ namespace Avalonia.Animation
///
/// Handles the timing and lifetime of a .
///
- internal class TransitionInstance : SingleSubscriberObservableBase
+ internal class TransitionInstance : SingleSubscriberObservableBase, IObserver
{
private IDisposable _timerSubscription;
private TimeSpan _delay;
private TimeSpan _duration;
private readonly IClock _baseClock;
- private IClock _clock;
+ private TransitionClock _clock;
public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration)
{
@@ -75,9 +72,56 @@ namespace Avalonia.Animation
protected override void Subscribed()
{
- _clock = new Clock(_baseClock);
- _timerSubscription = _clock.Subscribe(TimerTick);
+ _clock = new TransitionClock(_baseClock);
+ _timerSubscription = _clock.Subscribe(this);
PublishNext(0.0d);
}
+
+ void IObserver.OnCompleted()
+ {
+ PublishCompleted();
+ }
+
+ void IObserver.OnError(Exception error)
+ {
+ PublishError(error);
+ }
+
+ void IObserver.OnNext(TimeSpan value)
+ {
+ TimerTick(value);
+ }
+
+ ///
+ /// TODO: This clock is still fairly expensive due to implementation.
+ ///
+ private sealed class TransitionClock : ClockBase, IObserver
+ {
+ private readonly IDisposable _parentSubscription;
+
+ public TransitionClock(IClock parent)
+ {
+ _parentSubscription = parent.Subscribe(this);
+ }
+
+ protected override void Stop()
+ {
+ _parentSubscription.Dispose();
+ }
+
+ void IObserver.OnNext(TimeSpan value)
+ {
+ Pulse(value);
+ }
+
+ void IObserver.OnCompleted()
+ {
+ }
+
+ void IObserver.OnError(Exception error)
+ {
+ ExceptionDispatchInfo.Capture(error).Throw();
+ }
+ }
}
}
diff --git a/src/Avalonia.Animation/TransitionObservableBase.cs b/src/Avalonia.Animation/TransitionObservableBase.cs
new file mode 100644
index 0000000000..c4ac803135
--- /dev/null
+++ b/src/Avalonia.Animation/TransitionObservableBase.cs
@@ -0,0 +1,58 @@
+using System;
+using Avalonia.Animation.Easings;
+using Avalonia.Reactive;
+
+#nullable enable
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Provides base for observables implementing transitions.
+ ///
+ /// Type of the transitioned value.
+ public abstract class TransitionObservableBase : SingleSubscriberObservableBase, IObserver
+ {
+ private readonly Easing _easing;
+ private readonly IObservable _progress;
+ private IDisposable? _progressSubscription;
+
+ protected TransitionObservableBase(IObservable progress, Easing easing)
+ {
+ _progress = progress;
+ _easing = easing;
+ }
+
+ ///
+ /// Produces value at given progress time point.
+ ///
+ /// Transition progress.
+ protected abstract T ProduceValue(double progress);
+
+ protected override void Subscribed()
+ {
+ _progressSubscription = _progress.Subscribe(this);
+ }
+
+ protected override void Unsubscribed()
+ {
+ _progressSubscription?.Dispose();
+ }
+
+ void IObserver.OnCompleted()
+ {
+ PublishCompleted();
+ }
+
+ void IObserver.OnError(Exception error)
+ {
+ PublishError(error);
+ }
+
+ void IObserver.OnNext(double value)
+ {
+ double progress = _easing.Ease(value);
+
+ PublishNext(ProduceValue(progress));
+ }
+ }
+}
diff --git a/src/Avalonia.Animation/Transitions/DoubleTransition.cs b/src/Avalonia.Animation/Transitions/DoubleTransition.cs
index 8cae1e1f81..7232d87863 100644
--- a/src/Avalonia.Animation/Transitions/DoubleTransition.cs
+++ b/src/Avalonia.Animation/Transitions/DoubleTransition.cs
@@ -1,22 +1,11 @@
-using System;
-using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
///
/// Transition class that handles with types.
///
- public class DoubleTransition : Transition
+ public class DoubleTransition : AnimatorDrivenTransition
{
- ///
- public override IObservable DoTransition(IObservable progress, double oldValue, double newValue)
- {
- return progress
- .Select(p =>
- {
- var f = Easing.Ease(p);
- return ((newValue - oldValue) * f) + oldValue;
- });
- }
}
}
diff --git a/src/Avalonia.Animation/Transitions/FloatTransition.cs b/src/Avalonia.Animation/Transitions/FloatTransition.cs
index 427563e559..a96db8ba5b 100644
--- a/src/Avalonia.Animation/Transitions/FloatTransition.cs
+++ b/src/Avalonia.Animation/Transitions/FloatTransition.cs
@@ -1,19 +1,11 @@
-using System;
-using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
///
/// Transition class that handles with types.
///
- public class FloatTransition : Transition
+ public class FloatTransition : AnimatorDrivenTransition
{
- ///
- public override IObservable DoTransition(IObservable progress, float oldValue, float newValue)
- {
- var delta = newValue - oldValue;
- return progress
- .Select(p => (float)Easing.Ease(p) * delta + oldValue);
- }
}
}
diff --git a/src/Avalonia.Animation/Transitions/IntegerTransition.cs b/src/Avalonia.Animation/Transitions/IntegerTransition.cs
index 7a85bd75dc..343da7b689 100644
--- a/src/Avalonia.Animation/Transitions/IntegerTransition.cs
+++ b/src/Avalonia.Animation/Transitions/IntegerTransition.cs
@@ -1,19 +1,11 @@
-using System;
-using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
///
/// Transition class that handles with types.
///
- public class IntegerTransition : Transition
+ public class IntegerTransition : AnimatorDrivenTransition
{
- ///
- public override IObservable DoTransition(IObservable progress, int oldValue, int newValue)
- {
- var delta = newValue - oldValue;
- return progress
- .Select(p => (int)(Easing.Ease(p) * delta + oldValue));
- }
}
}
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index fdd688cf9d..6a9cff6b71 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -7,6 +7,8 @@ using Avalonia.Logging;
using Avalonia.PropertyStore;
using Avalonia.Threading;
+#nullable enable
+
namespace Avalonia
{
///
@@ -17,12 +19,12 @@ namespace Avalonia
///
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink
{
- private IAvaloniaObject _inheritanceParent;
- private List _directBindings;
- private PropertyChangedEventHandler _inpcChanged;
- private EventHandler _propertyChanged;
- private List _inheritanceChildren;
- private ValueStore _values;
+ private IAvaloniaObject? _inheritanceParent;
+ private List? _directBindings;
+ private PropertyChangedEventHandler? _inpcChanged;
+ private EventHandler? _propertyChanged;
+ private List? _inheritanceChildren;
+ private ValueStore? _values;
private bool _batchUpdate;
///
@@ -36,7 +38,7 @@ namespace Avalonia
///
/// Raised when a value changes on this object.
///
- public event EventHandler PropertyChanged
+ public event EventHandler? PropertyChanged
{
add { _propertyChanged += value; }
remove { _propertyChanged -= value; }
@@ -58,7 +60,7 @@ namespace Avalonia
///
/// The inheritance parent.
///
- protected IAvaloniaObject InheritanceParent
+ protected IAvaloniaObject? InheritanceParent
{
get
{
@@ -289,7 +291,8 @@ namespace Avalonia
/// True if the property is animating, otherwise false.
public bool IsAnimating(AvaloniaProperty property)
{
- Contract.Requires(property != null);
+ property = property ?? throw new ArgumentNullException(nameof(property));
+
VerifyAccess();
return _values?.IsAnimating(property) ?? false;
@@ -306,7 +309,8 @@ namespace Avalonia
///
public bool IsSet(AvaloniaProperty property)
{
- Contract.Requires(property != null);
+ property = property ?? throw new ArgumentNullException(nameof(property));
+
VerifyAccess();
return _values?.IsSet(property) ?? false;
@@ -320,7 +324,7 @@ namespace Avalonia
/// The priority of the value.
public void SetValue(
AvaloniaProperty property,
- object value,
+ object? value,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
@@ -338,7 +342,7 @@ namespace Avalonia
///
/// An if setting the property can be undone, otherwise null.
///
- public IDisposable SetValue(
+ public IDisposable? SetValue(
StyledPropertyBase property,
T value,
BindingPriority priority = BindingPriority.LocalValue)
@@ -497,7 +501,7 @@ namespace Avalonia
}
///
- Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
+ Delegate[]? IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
{
return _propertyChanged?.GetInvocationList();
}
@@ -723,7 +727,8 @@ namespace Avalonia
{
var values = o._values;
- if (values?.TryGetValue(property, maxPriority, out value) == true)
+ if (values != null
+ && values.TryGetValue(property, maxPriority, out value) == true)
{
return value;
}
@@ -873,7 +878,7 @@ namespace Avalonia
}
else
{
- LogBindingError(property, value.Error);
+ LogBindingError(property, value.Error!);
}
}
}
@@ -907,14 +912,14 @@ namespace Avalonia
{
_owner = owner;
_property = property;
- _owner._directBindings.Add(this);
+ _owner._directBindings!.Add(this);
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription.Dispose();
- _owner._directBindings.Remove(this);
+ _owner._directBindings!.Remove(this);
}
public void OnCompleted() => Dispose();
diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs
index 173c5c1a94..ae61f8f642 100644
--- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs
+++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs
@@ -585,6 +585,30 @@ namespace Avalonia
});
}
+ ///
+ /// Subscribes to a property changed notifications for changes that originate from a
+ /// .
+ ///
+ /// The type of the property change sender.
+ /// /// The type of the property..
+ /// The property changed observable.
+ ///
+ /// The method to call. The parameters are the sender and the event args.
+ ///
+ /// A disposable that can be used to terminate the subscription.
+ public static IDisposable AddClassHandler(
+ this IObservable> observable,
+ Action> action) where TTarget : AvaloniaObject
+ {
+ return observable.Subscribe(e =>
+ {
+ if (e.Sender is TTarget target)
+ {
+ action(target, e);
+ }
+ });
+ }
+
///
/// Subscribes to a property changed notifications for changes that originate from a
/// .
diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs
index e79980518f..93948e54ee 100644
--- a/src/Avalonia.Base/Data/BindingValue.cs
+++ b/src/Avalonia.Base/Data/BindingValue.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Utilities;
@@ -358,6 +359,7 @@ namespace Avalonia.Data
e);
}
+ [Conditional("DEBUG")]
private static void ValidateValue([AllowNull] T value)
{
if (value is UnsetValueType)
diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs
index 023999f5c5..e868b71fcd 100644
--- a/src/Avalonia.Base/Data/Core/StreamNode.cs
+++ b/src/Avalonia.Base/Data/Core/StreamNode.cs
@@ -20,7 +20,7 @@ namespace Avalonia.Data.Core
protected override void StartListeningCore(WeakReference reference)
{
- GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged);
+ _subscription = GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged);
}
protected override void StopListeningCore()
diff --git a/src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs b/src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs
index 7f09425905..4b9f12ddf8 100644
--- a/src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs
+++ b/src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs
@@ -1,5 +1,7 @@
using System;
+#nullable enable
+
namespace Avalonia.Diagnostics
{
///
@@ -14,6 +16,6 @@ namespace Avalonia.Diagnostics
///
/// The subscribers or null if no subscribers.
///
- Delegate[] GetPropertyChangedSubscribers();
+ Delegate[]? GetPropertyChangedSubscribers();
}
}
diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs
index 0452f77d4c..2e992f8616 100644
--- a/src/Avalonia.Base/IAvaloniaObject.cs
+++ b/src/Avalonia.Base/IAvaloniaObject.cs
@@ -1,6 +1,8 @@
using System;
using Avalonia.Data;
+#nullable enable
+
namespace Avalonia
{
///
@@ -11,7 +13,7 @@ namespace Avalonia
///
/// Raised when a value changes on this object.
///
- event EventHandler PropertyChanged;
+ event EventHandler? PropertyChanged;
///
/// Clears an 's local value.
@@ -75,7 +77,10 @@ namespace Avalonia
/// The property.
/// The value.
/// The priority of the value.
- IDisposable SetValue(
+ ///
+ /// An if setting the property can be undone, otherwise null.
+ ///
+ IDisposable? SetValue(
StyledPropertyBase property,
T value,
BindingPriority priority = BindingPriority.LocalValue);
diff --git a/src/Avalonia.Base/Metadata/NullableAttributes.cs b/src/Avalonia.Base/Metadata/NullableAttributes.cs
index 91f5e81863..b6f0f3a47c 100644
--- a/src/Avalonia.Base/Metadata/NullableAttributes.cs
+++ b/src/Avalonia.Base/Metadata/NullableAttributes.cs
@@ -1,6 +1,5 @@
#pragma warning disable MA0048 // File name must match type name
#define INTERNAL_NULLABLE_ATTRIBUTES
-#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
// https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs
@@ -10,6 +9,7 @@
namespace System.Diagnostics.CodeAnalysis
{
+#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
/// Specifies that null is allowed as an input even if the corresponding type disallows it.
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
#if INTERNAL_NULLABLE_ATTRIBUTES
@@ -136,5 +136,82 @@ namespace System.Diagnostics.CodeAnalysis
/// Gets the condition parameter value.
public bool ParameterValue { get; }
}
-}
+#endif // NETSTANDARD2_0 attributes
+
+#if NETSTANDARD2_1 || NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
+ ///
+ /// Specifies that the method or property will ensure that the listed field and property members have
+ /// not- values.
+ ///
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
+#if INTERNAL_NULLABLE_ATTRIBUTES
+ internal
+#else
+ public
#endif
+ sealed class MemberNotNullAttribute : Attribute
+ {
+ /// Gets field or property member names.
+ public string[] Members { get; }
+
+ /// Initializes the attribute with a field or property member.
+ /// The field or property member that is promised to be not-null.
+ public MemberNotNullAttribute(string member)
+ {
+ Members = new[] { member };
+ }
+
+ /// Initializes the attribute with the list of field and property members.
+ /// The list of field and property members that are promised to be not-null.
+ public MemberNotNullAttribute(params string[] members)
+ {
+ Members = members;
+ }
+ }
+
+ ///
+ /// Specifies that the method or property will ensure that the listed field and property members have
+ /// non- values when returning with the specified return value condition.
+ ///
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
+#if INTERNAL_NULLABLE_ATTRIBUTES
+ internal
+#else
+ public
+#endif
+ sealed class MemberNotNullWhenAttribute : Attribute
+ {
+ /// Gets the return value condition.
+ public bool ReturnValue { get; }
+
+ /// Gets field or property member names.
+ public string[] Members { get; }
+
+ /// Initializes the attribute with the specified return value condition and a field or property member.
+ ///
+ /// The return value condition. If the method returns this value,
+ /// the associated parameter will not be .
+ ///
+ /// The field or property member that is promised to be not- .
+ public MemberNotNullWhenAttribute(bool returnValue, string member)
+ {
+ ReturnValue = returnValue;
+ Members = new[] { member };
+ }
+
+ /// Initializes the attribute with the specified return value condition and list of field and property members.
+ ///
+ ///
+ /// The return value condition. If the method returns this value,
+ /// the associated parameter will not be .
+ ///
+ /// The list of field and property members that are promised to be not-null.
+ public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
+ {
+ ReturnValue = returnValue;
+ Members = members;
+ }
+ }
+#endif // NETSTANDARD2_1 attributes
+}
+
diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs
index 1b29338f07..362736eb06 100644
--- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs
@@ -79,7 +79,7 @@ namespace Avalonia.PropertyStore
public void OnError(Exception error)
{
- throw new NotImplementedException();
+ throw new NotImplementedException("BindingEntry.OnError is not implemented", error);
}
public void OnNext(BindingValue value)
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index a070a1b9d7..83f13fe199 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -75,7 +75,6 @@ namespace Avalonia.Controls
private const double DATAGRID_defaultMinColumnWidth = 20;
private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity;
- private List _validationErrors;
private List _bindingValidationErrors;
private IDisposable _validationSubscription;
@@ -102,7 +101,6 @@ namespace Avalonia.Controls
private bool _areHandlersSuspended;
private bool _autoSizingColumns;
private IndexToValueTable _collapsedSlotsTable;
- private DataGridCellCoordinates _currentCellCoordinates;
private Control _clickedElement;
// used to store the current column during a Reset
@@ -141,7 +139,6 @@ namespace Avalonia.Controls
private DataGridSelectedItemsCollection _selectedItems;
private bool _temporarilyResetCurrentCell;
private object _uneditedValue; // Represents the original current cell value at the time it enters editing mode.
- private ICellEditBinding _currentCellEditBinding;
// An approximation of the sum of the heights in pixels of the scrolling rows preceding
// the first displayed scrolling row. Since the scrolled off rows are discarded, the grid
@@ -535,7 +532,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect(
nameof(SelectedIndex),
o => o.SelectedIndex,
- (o, v) => o.SelectedIndex = v);
+ (o, v) => o.SelectedIndex = v,
+ defaultBindingMode: BindingMode.TwoWay);
///
/// Gets or sets the index of the current selection.
@@ -553,7 +551,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect(
nameof(SelectedItem),
o => o.SelectedItem,
- (o, v) => o.SelectedItem = v);
+ (o, v) => o.SelectedItem = v,
+ defaultBindingMode: BindingMode.TwoWay);
///
/// Gets or sets the data item corresponding to the selected row.
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs
index a69b8eafe1..4bfbd7d818 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs
@@ -425,7 +425,7 @@ namespace Avalonia.Controls
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight);
}
- if (DisplayData.FirstScrollingSlot < slot && DisplayData.LastScrollingSlot > slot)
+ if (DisplayData.FirstScrollingSlot < slot && (DisplayData.LastScrollingSlot > slot || DisplayData.LastScrollingSlot == -1))
{
// The row is already displayed in its entirety
return true;
diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
index 998198cc1c..0c4f8249ce 100644
--- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
+++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
@@ -163,6 +163,10 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs
new file mode 100644
index 0000000000..66fad557d5
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs
@@ -0,0 +1,52 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics.Controls
+{
+ internal class FilterTextBox : TextBox, IStyleable
+ {
+ public static readonly DirectProperty UseRegexFilterProperty =
+ AvaloniaProperty.RegisterDirect(nameof(UseRegexFilter),
+ o => o.UseRegexFilter, (o, v) => o.UseRegexFilter = v,
+ defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly DirectProperty UseCaseSensitiveFilterProperty =
+ AvaloniaProperty.RegisterDirect(nameof(UseCaseSensitiveFilter),
+ o => o.UseCaseSensitiveFilter, (o, v) => o.UseCaseSensitiveFilter = v,
+ defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly DirectProperty UseWholeWordFilterProperty =
+ AvaloniaProperty.RegisterDirect(nameof(UseWholeWordFilter),
+ o => o.UseWholeWordFilter, (o, v) => o.UseWholeWordFilter = v,
+ defaultBindingMode: BindingMode.TwoWay);
+
+ private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
+
+ public FilterTextBox()
+ {
+ Classes.Add("filter-text-box");
+ }
+
+ public bool UseRegexFilter
+ {
+ get => _useRegexFilter;
+ set => SetAndRaise(UseRegexFilterProperty, ref _useRegexFilter, value);
+ }
+
+ public bool UseCaseSensitiveFilter
+ {
+ get => _useCaseSensitiveFilter;
+ set => SetAndRaise(UseCaseSensitiveFilterProperty, ref _useCaseSensitiveFilter, value);
+ }
+
+ public bool UseWholeWordFilter
+ {
+ get => _useWholeWordFilter;
+ set => SetAndRaise(UseWholeWordFilterProperty, ref _useWholeWordFilter, value);
+ }
+
+ Type IStyleable.StyleKey => typeof(TextBox);
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml
new file mode 100644
index 0000000000..c202d13778
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
similarity index 87%
rename from src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs
rename to src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
index c7611c8c46..cb98fb70f3 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
@@ -1,29 +1,17 @@
using Avalonia.Controls;
using Avalonia.Data;
-using Avalonia.Data.Converters;
using Avalonia.Media;
-namespace Avalonia.Diagnostics.Views
+namespace Avalonia.Diagnostics.Controls
{
- internal static class Converters
- {
- public static IValueConverter HasConstraintConverter =
- new FuncValueConverter(ConvertToDecoration);
-
- private static TextDecorationCollection ConvertToDecoration(object arg)
- {
- return arg != null ? TextDecorations.Underline : null;
- }
- }
-
internal class ThicknessEditor : ContentControl
{
public static readonly DirectProperty ThicknessProperty =
AvaloniaProperty.RegisterDirect(nameof(Thickness), o => o.Thickness,
(o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay);
- public static readonly DirectProperty HeaderProperty =
- AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header,
+ public static readonly DirectProperty HeaderProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header,
(o, v) => o.Header = v);
public static readonly DirectProperty IsPresentProperty =
@@ -44,15 +32,16 @@ namespace Avalonia.Diagnostics.Views
AvaloniaProperty.RegisterDirect(nameof(Bottom), o => o.Bottom,
(o, v) => o.Bottom = v);
-
+ public static readonly StyledProperty HighlightProperty =
+ AvaloniaProperty.Register(nameof(Highlight));
+
private Thickness _thickness;
- private string _header;
+ private string? _header;
private bool _isPresent = true;
private double _left;
private double _top;
private double _right;
private double _bottom;
-
private bool _isUpdatingThickness;
public Thickness Thickness
@@ -61,7 +50,7 @@ namespace Avalonia.Diagnostics.Views
set => SetAndRaise(ThicknessProperty, ref _thickness, value);
}
- public string Header
+ public string? Header
{
get => _header;
set => SetAndRaise(HeaderProperty, ref _header, value);
@@ -97,6 +86,12 @@ namespace Avalonia.Diagnostics.Views
set => SetAndRaise(BottomProperty, ref _bottom, value);
}
+ public IBrush Highlight
+ {
+ get => GetValue(HighlightProperty);
+ set => SetValue(HighlightProperty, value);
+ }
+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
deleted file mode 100644
index 37ba5155fd..0000000000
--- a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-using System.Globalization;
-using Avalonia.Data.Converters;
-using Avalonia.Media;
-
-namespace Avalonia.Diagnostics.Converters
-{
- internal class BoolToBrushConverter : IValueConverter
- {
- public IBrush Brush { get; set; }
-
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- return (bool)value ? Brush : Brushes.Transparent;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
index 63ac3ab62f..0b9044e65e 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
@@ -8,12 +8,17 @@ namespace Avalonia.Diagnostics.Converters
{
public double Opacity { get; set; }
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
- return (bool)value ? 1d : Opacity;
+ if (value is bool boolean && boolean)
+ {
+ return 1d;
+ }
+
+ return Opacity;
}
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs
new file mode 100644
index 0000000000..4863782f44
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Globalization;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+
+namespace Avalonia.Diagnostics.Converters
+{
+ internal class EnumToCheckedConverter : IValueConverter
+ {
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return Equals(value, parameter);
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool isChecked && isChecked)
+ {
+ return parameter;
+ }
+
+ return BindingOperations.DoNothing;
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
index 7942d22962..2a386f106e 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
@@ -6,13 +6,12 @@ using Avalonia.Diagnostics.Views;
using Avalonia.Input;
using Avalonia.Interactivity;
-#nullable enable
-
namespace Avalonia.Diagnostics
{
public static class DevTools
{
- private static readonly Dictionary s_open = new Dictionary();
+ private static readonly Dictionary s_open =
+ new Dictionary();
public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{
@@ -24,7 +23,7 @@ namespace Avalonia.Diagnostics
public static IDisposable Attach(TopLevel root, DevToolsOptions options)
{
- void PreviewKeyDown(object sender, KeyEventArgs e)
+ void PreviewKeyDown(object? sender, KeyEventArgs e)
{
if (options.Gesture.Matches(e))
{
@@ -54,6 +53,7 @@ namespace Avalonia.Diagnostics
Width = options.Size.Width,
Height = options.Size.Height,
};
+ window.SetOptions(options);
window.Closed += DevToolsClosed;
s_open.Add(root, window);
@@ -71,10 +71,10 @@ namespace Avalonia.Diagnostics
return Disposable.Create(() => window?.Close());
}
- private static void DevToolsClosed(object sender, EventArgs e)
+ private static void DevToolsClosed(object? sender, EventArgs e)
{
- var window = (MainWindow)sender;
- s_open.Remove(window.Root);
+ var window = (MainWindow)sender!;
+ s_open.Remove(window.Root!);
window.Closed -= DevToolsClosed;
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
index ee46192207..5336dca65b 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
@@ -19,8 +19,13 @@ namespace Avalonia.Diagnostics
public bool ShowAsChildWindow { get; set; } = true;
///
- /// Gets or sets the initial size of the DevTools window. The default value is 1024x512.
+ /// Gets or sets the initial size of the DevTools window. The default value is 1280x720.
///
- public Size Size { get; set; } = new Size(1024, 512);
+ public Size Size { get; set; } = new Size(1280, 720);
+
+ ///
+ /// Get or set the startup screen index where the DevTools window will be displayed.
+ ///
+ public int? StartupScreenIndex { get; set; }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
index 5927bd785e..4f4579c7d9 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
@@ -22,8 +22,8 @@ The following commands are available:
clear(): Clear the output history
";
- public dynamic e { get; internal set; }
- public dynamic root { get; internal set; }
+ public dynamic? e { get; internal set; }
+ public dynamic? root { get; internal set; }
internal static object NoOutput { get; } = new object();
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
index 65117dc3d1..4f493bdcc2 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
@@ -7,15 +7,15 @@ namespace Avalonia.Diagnostics.Models
{
public EventChainLink(object handler, bool handled, RoutingStrategies route)
{
- Contract.Requires(handler != null);
-
- Handler = handler;
+ Handler = handler ?? throw new ArgumentNullException(nameof(handler));
Handled = handled;
Route = route;
}
public object Handler { get; }
+ public bool BeginsNewRoute { get; set; }
+
public string HandlerName
{
get
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
index be3564e781..16852001da 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
@@ -9,12 +9,12 @@ namespace Avalonia.Diagnostics
{
public IControl Build(object data)
{
- var name = data.GetType().FullName.Replace("ViewModel", "View");
+ var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
- return (Control)Activator.CreateInstance(type);
+ return (Control)Activator.CreateInstance(type)!;
}
else
{
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
index a9353eba8b..e4c4ca6115 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
@@ -1,17 +1,17 @@
-using System.ComponentModel;
-using Avalonia.Collections;
-
namespace Avalonia.Diagnostics.ViewModels
{
internal class AvaloniaPropertyViewModel : PropertyViewModel
{
private readonly AvaloniaObject _target;
private string _type;
- private object _value;
+ private object? _value;
private string _priority;
private string _group;
+#nullable disable
+ // Remove "nullable disable" after MemberNotNull will work on our CI.
public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property)
+#nullable restore
{
_target = o;
Property = property;
@@ -20,25 +20,17 @@ namespace Avalonia.Diagnostics.ViewModels
$"[{property.OwnerType.Name}.{property.Name}]" :
property.Name;
- if (property.IsDirect)
- {
- _group = "Properties";
- Priority = "Direct";
- }
-
Update();
}
public AvaloniaProperty Property { get; }
public override object Key => Property;
public override string Name { get; }
- public bool IsAttached => Property.IsAttached;
+ public override bool? IsAttached =>
+ Property.IsAttached;
- public string Priority
- {
- get => _priority;
- private set => RaiseAndSetIfChanged(ref _priority, value);
- }
+ public override string Priority =>
+ _priority;
public override string Type => _type;
@@ -56,40 +48,37 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- public override string Group
- {
- get => _group;
- }
+ public override string Group => _group;
+ // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
public override void Update()
{
if (Property.IsDirect)
{
RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value));
- RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
+ RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type));
+ RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority));
+
+ _group = "Properties";
}
else
{
var val = _target.GetDiagnostic(Property);
RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value));
- RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
+ RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type));
if (val != null)
{
- SetGroup(IsAttached ? "Attached Properties" : "Properties");
- Priority = val.Priority.ToString();
+ RaiseAndSetIfChanged(ref _priority, val.Priority.ToString(), nameof(Priority));
+ RaiseAndSetIfChanged(ref _group, IsAttached == true ? "Attached Properties" : "Properties", nameof(Group));
}
else
{
- SetGroup(Priority = "Unset");
+ RaiseAndSetIfChanged(ref _priority, "Unset", nameof(Priority));
+ RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group));
}
}
}
-
- private void SetGroup(string group)
- {
- RaiseAndSetIfChanged(ref _group, group, nameof(Group));
- }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
index af5e254204..65626aeea5 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
@@ -1,5 +1,4 @@
-using System.ComponentModel;
-using System.Reflection;
+using System.Reflection;
namespace Avalonia.Diagnostics.ViewModels
{
@@ -7,14 +6,17 @@ namespace Avalonia.Diagnostics.ViewModels
{
private readonly object _target;
private string _type;
- private object _value;
+ private object? _value;
+#nullable disable
+ // Remove "nullable disable" after MemberNotNull will work on our CI.
public ClrPropertyViewModel(object o, PropertyInfo property)
+#nullable restore
{
_target = o;
Property = property;
- if (!property.DeclaringType.IsInterface)
+ if (property.DeclaringType == null || !property.DeclaringType.IsInterface)
{
Name = property.Name;
}
@@ -47,11 +49,18 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
+ public override string Priority =>
+ string.Empty;
+
+ public override bool? IsAttached =>
+ default;
+
+ // [MemberNotNull(nameof(_type))]
public override void Update()
{
var val = Property.GetValue(_target);
RaiseAndSetIfChanged(ref _value, val, nameof(Value));
- RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
+ RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type));
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
index 0e0c44ded8..717b49d074 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
@@ -15,11 +15,12 @@ namespace Avalonia.Diagnostics.ViewModels
private int _historyIndex = -1;
private string _input;
private bool _isVisible;
- private ScriptState _state;
+ private ScriptState? _state;
public ConsoleViewModel(Action updateContext)
{
_context = new ConsoleContext(this);
+ _input = string.Empty;
_updateContext = updateContext;
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
index 32592559e5..3790951b0c 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
@@ -18,11 +18,10 @@ namespace Avalonia.Diagnostics.ViewModels
{
private readonly IVisual _control;
private readonly IDictionary> _propertyIndex;
- private AvaloniaPropertyViewModel _selectedProperty;
- private string _styleFilter;
+ private PropertyViewModel? _selectedProperty;
private bool _snapshotStyles;
private bool _showInactiveStyles;
- private string _styleStatus;
+ private string? _styleStatus;
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
{
@@ -84,7 +83,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
foreach (var setter in style.Setters)
{
- if (setter is Setter regularSetter)
+ if (setter is Setter regularSetter
+ && regularSetter.Property != null)
{
var setterValue = regularSetter.Value;
@@ -116,13 +116,14 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- private (object resourceKey, bool isDynamic)? GetResourceInfo(object value)
+ private (object resourceKey, bool isDynamic)? GetResourceInfo(object? value)
{
if (value is StaticResourceExtension staticResource)
{
return (staticResource.ResourceKey, false);
}
- else if (value is DynamicResourceExtension dynamicResource)
+ else if (value is DynamicResourceExtension dynamicResource
+ && dynamicResource.ResourceKey != null)
{
return (dynamicResource.ResourceKey, true);
}
@@ -138,18 +139,12 @@ namespace Avalonia.Diagnostics.ViewModels
public ObservableCollection PseudoClasses { get; }
- public AvaloniaPropertyViewModel SelectedProperty
+ public PropertyViewModel? SelectedProperty
{
get => _selectedProperty;
set => RaiseAndSetIfChanged(ref _selectedProperty, value);
}
- public string StyleFilter
- {
- get => _styleFilter;
- set => RaiseAndSetIfChanged(ref _styleFilter, value);
- }
-
public bool SnapshotStyles
{
get => _snapshotStyles;
@@ -162,7 +157,7 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _showInactiveStyles, value);
}
- public string StyleStatus
+ public string? StyleStatus
{
get => _styleStatus;
set => RaiseAndSetIfChanged(ref _styleStatus, value);
@@ -174,11 +169,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
base.OnPropertyChanged(e);
- if (e.PropertyName == nameof(StyleFilter))
- {
- UpdateStyleFilters();
- }
- else if (e.PropertyName == nameof(SnapshotStyles))
+ if (e.PropertyName == nameof(SnapshotStyles))
{
if (!SnapshotStyles)
{
@@ -187,19 +178,15 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- private void UpdateStyleFilters()
+ public void UpdateStyleFilters()
{
- var filter = StyleFilter;
- bool hasFilter = !string.IsNullOrEmpty(filter);
-
foreach (var style in AppliedStyles)
{
var hasVisibleSetter = false;
foreach (var setter in style.Setters)
{
- setter.IsVisible =
- !hasFilter || setter.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
+ setter.IsVisible = TreePage.SettersFilter.Filter(setter.Name);
hasVisibleSetter |= setter.IsVisible;
}
@@ -263,7 +250,7 @@ namespace Avalonia.Diagnostics.ViewModels
.Select(x => new ClrPropertyViewModel(o, x));
}
- private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+ private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (_propertyIndex.TryGetValue(e.Property, out var properties))
{
@@ -276,9 +263,10 @@ namespace Avalonia.Diagnostics.ViewModels
Layout.ControlPropertyChanged(sender, e);
}
- private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e)
+ private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
- if (_propertyIndex.TryGetValue(e.PropertyName, out var properties))
+ if (e.PropertyName != null
+ && _propertyIndex.TryGetValue(e.PropertyName, out var properties))
{
foreach (var property in properties)
{
@@ -292,7 +280,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
+ private void OnClassesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (!SnapshotStyles)
{
@@ -357,27 +345,17 @@ namespace Avalonia.Diagnostics.ViewModels
private bool FilterProperty(object arg)
{
- if (!string.IsNullOrWhiteSpace(TreePage.PropertyFilter) && arg is PropertyViewModel property)
- {
- if (TreePage.UseRegexFilter)
- {
- return TreePage.FilterRegex?.IsMatch(property.Name) ?? true;
- }
-
- return property.Name.IndexOf(TreePage.PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1;
- }
-
- return true;
+ return !(arg is PropertyViewModel property) || TreePage.PropertiesFilter.Filter(property.Name);
}
private class PropertyComparer : IComparer
{
public static PropertyComparer Instance { get; } = new PropertyComparer();
- public int Compare(PropertyViewModel x, PropertyViewModel y)
+ public int Compare(PropertyViewModel? x, PropertyViewModel? y)
{
- var groupX = GroupIndex(x.Group);
- var groupY = GroupIndex(y.Group);
+ var groupX = GroupIndex(x?.Group);
+ var groupY = GroupIndex(y?.Group);
if (groupX != groupY)
{
@@ -385,11 +363,11 @@ namespace Avalonia.Diagnostics.ViewModels
}
else
{
- return string.CompareOrdinal(x.Name, y.Name);
+ return string.CompareOrdinal(x?.Name, y?.Name);
}
}
- private int GroupIndex(string group)
+ private int GroupIndex(string? group)
{
switch (group)
{
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
index fd2e4c3355..4dc0c34c0a 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
@@ -1,4 +1,6 @@
+using System;
using System.ComponentModel;
+using System.Text;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.VisualTree;
@@ -8,27 +10,58 @@ namespace Avalonia.Diagnostics.ViewModels
internal class ControlLayoutViewModel : ViewModelBase
{
private readonly IVisual _control;
- private Thickness _marginThickness;
private Thickness _borderThickness;
- private Thickness _paddingThickness;
- private double _width;
private double _height;
- private string _widthConstraint;
- private string _heightConstraint;
+ private string? _heightConstraint;
+ private HorizontalAlignment _horizontalAlignment;
+ private Thickness _marginThickness;
+ private Thickness _paddingThickness;
private bool _updatingFromControl;
+ private VerticalAlignment _verticalAlignment;
+ private double _width;
+ private string? _widthConstraint;
+
+ public ControlLayoutViewModel(IVisual control)
+ {
+ _control = control;
+
+ HasPadding = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Decorator.PaddingProperty);
+ HasBorder = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Border.BorderThicknessProperty);
+
+ if (control is AvaloniaObject ao)
+ {
+ MarginThickness = ao.GetValue(Layoutable.MarginProperty);
+
+ if (HasPadding)
+ {
+ PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
+ }
+
+ if (HasBorder)
+ {
+ BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
+ }
+
+ HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty);
+ VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty);
+ }
+
+ UpdateSize();
+ UpdateSizeConstraints();
+ }
public Thickness MarginThickness
{
get => _marginThickness;
set => RaiseAndSetIfChanged(ref _marginThickness, value);
}
-
+
public Thickness BorderThickness
{
get => _borderThickness;
set => RaiseAndSetIfChanged(ref _borderThickness, value);
}
-
+
public Thickness PaddingThickness
{
get => _paddingThickness;
@@ -47,60 +80,61 @@ namespace Avalonia.Diagnostics.ViewModels
private set => RaiseAndSetIfChanged(ref _height, value);
}
- public string WidthConstraint
+ public string? WidthConstraint
{
get => _widthConstraint;
private set => RaiseAndSetIfChanged(ref _widthConstraint, value);
}
- public string HeightConstraint
+ public string? HeightConstraint
{
get => _heightConstraint;
private set => RaiseAndSetIfChanged(ref _heightConstraint, value);
}
- public bool HasPadding { get; }
-
- public bool HasBorder { get; }
-
- public ControlLayoutViewModel(IVisual control)
+ public HorizontalAlignment HorizontalAlignment
{
- _control = control;
-
- HasPadding = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Decorator.PaddingProperty);
- HasBorder = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Border.BorderThicknessProperty);
-
- if (control is AvaloniaObject ao)
- {
- MarginThickness = ao.GetValue(Layoutable.MarginProperty);
+ get => _horizontalAlignment;
+ private set => RaiseAndSetIfChanged(ref _horizontalAlignment, value);
+ }
- if (HasPadding)
- {
- PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
- }
+ public VerticalAlignment VerticalAlignment
+ {
+ get => _verticalAlignment;
+ private set => RaiseAndSetIfChanged(ref _verticalAlignment, value);
+ }
- if (HasBorder)
- {
- BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
- }
- }
+ public bool HasPadding { get; }
- UpdateSize();
- UpdateSizeConstraints();
- }
+ public bool HasBorder { get; }
private void UpdateSizeConstraints()
{
if (_control is IAvaloniaObject ao)
{
- string CreateConstraintInfo(StyledProperty minProperty, StyledProperty maxProperty)
+ string? CreateConstraintInfo(StyledProperty minProperty, StyledProperty maxProperty)
{
- if (ao.IsSet(minProperty) || ao.IsSet(maxProperty))
+ bool hasMin = ao.IsSet(minProperty);
+ bool hasMax = ao.IsSet(maxProperty);
+
+ if (hasMin || hasMax)
{
- var minValue = ao.GetValue(minProperty);
- var maxValue = ao.GetValue(maxProperty);
+ var builder = new StringBuilder();
+
+ if (hasMin)
+ {
+ var minValue = ao.GetValue(minProperty);
+ builder.AppendFormat("Min: {0}", Math.Round(minValue, 2));
+ builder.AppendLine();
+ }
- return $"{minValue} < size < {maxValue}";
+ if (hasMax)
+ {
+ var maxValue = ao.GetValue(maxProperty);
+ builder.AppendFormat("Max: {0}", Math.Round(maxValue, 2));
+ }
+
+ return builder.ToString();
}
return null;
@@ -134,10 +168,18 @@ namespace Avalonia.Diagnostics.ViewModels
{
ao.SetValue(Border.BorderThicknessProperty, BorderThickness);
}
+ else if (e.PropertyName == nameof(HorizontalAlignment))
+ {
+ ao.SetValue(Layoutable.HorizontalAlignmentProperty, HorizontalAlignment);
+ }
+ else if (e.PropertyName == nameof(VerticalAlignment))
+ {
+ ao.SetValue(Layoutable.VerticalAlignmentProperty, VerticalAlignment);
+ }
}
}
- public void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+ public void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
try
{
@@ -154,15 +196,15 @@ namespace Avalonia.Diagnostics.ViewModels
if (e.Property == Layoutable.MarginProperty)
{
MarginThickness = ao.GetValue(Layoutable.MarginProperty);
- }
+ }
else if (e.Property == Decorator.PaddingProperty)
{
PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
- }
+ }
else if (e.Property == Border.BorderThicknessProperty)
{
BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
- }
+ }
else if (e.Property == Layoutable.MinWidthProperty ||
e.Property == Layoutable.MaxWidthProperty ||
e.Property == Layoutable.MinHeightProperty ||
@@ -170,6 +212,14 @@ namespace Avalonia.Diagnostics.ViewModels
{
UpdateSizeConstraints();
}
+ else if (e.Property == Layoutable.HorizontalAlignmentProperty)
+ {
+ HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty);
+ }
+ else if (e.Property == Layoutable.VerticalAlignmentProperty)
+ {
+ VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty);
+ }
}
}
}
@@ -183,8 +233,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
var size = _control.Bounds;
- Width = size.Width;
- Height = size.Height;
+ Width = Math.Round(size.Width, 2);
+ Height = Math.Round(size.Height, 2);
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
index 60f247ead1..5b7ddc98ee 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
@@ -2,25 +2,17 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
-using Avalonia.Controls;
-using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventOwnerTreeNode : EventTreeNodeBase
{
- private static readonly RoutedEvent[] s_defaultEvents =
- {
- Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent,
- InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
- };
-
public EventOwnerTreeNode(Type type, IEnumerable events, EventsPageViewModel vm)
: base(null, type.Name)
{
Children = new AvaloniaList(events.OrderBy(e => e.Name)
- .Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) }));
+ .Select(e => new EventTreeNode(this, e, vm)));
IsExpanded = true;
}
@@ -35,7 +27,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (_updateChildren && value != null)
{
- foreach (var child in Children)
+ foreach (var child in Children!)
{
try
{
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
index 9291b3bdf7..65fd81cc78 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
@@ -9,21 +9,19 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventTreeNode : EventTreeNodeBase
{
- private readonly RoutedEvent _event;
private readonly EventsPageViewModel _parentViewModel;
private bool _isRegistered;
- private FiredEvent _currentEvent;
+ private FiredEvent? _currentEvent;
public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm)
: base(parent, @event.Name)
{
- Contract.Requires(@event != null);
- Contract.Requires(vm != null);
-
- _event = @event;
- _parentViewModel = vm;
+ Event = @event ?? throw new ArgumentNullException(nameof(@event));
+ _parentViewModel = vm ?? throw new ArgumentNullException(nameof(vm));
}
+ public RoutedEvent Event { get; }
+
public override bool? IsEnabled
{
get => base.IsEnabled;
@@ -53,24 +51,26 @@ namespace Avalonia.Diagnostics.ViewModels
{
if (IsEnabled.GetValueOrDefault() && !_isRegistered)
{
+ var allRoutes = RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble;
+
// FIXME: This leaks event handlers.
- _event.AddClassHandler(typeof(object), HandleEvent, (RoutingStrategies)7, handledEventsToo: true);
+ Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true);
_isRegistered = true;
}
}
- private void HandleEvent(object sender, RoutedEventArgs e)
+ private void HandleEvent(object? sender, RoutedEventArgs e)
{
if (!_isRegistered || IsEnabled == false)
return;
if (sender is IVisual v && BelongsToDevTool(v))
return;
- var s = sender;
+ var s = sender!;
var handled = e.Handled;
var route = e.Route;
- Action handler = delegate
+ void handler()
{
if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e))
{
@@ -95,14 +95,16 @@ namespace Avalonia.Diagnostics.ViewModels
private static bool BelongsToDevTool(IVisual v)
{
- while (v != null)
+ var current = v;
+
+ while (current != null)
{
- if (v is MainView || v is MainWindow)
+ if (current is MainView || current is MainWindow)
{
return true;
}
- v = v.VisualParent;
+ current = current.VisualParent;
}
return false;
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
index 1bd4117f23..e6d7335297 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
@@ -8,14 +8,16 @@ namespace Avalonia.Diagnostics.ViewModels
internal bool _updateParent = true;
private bool _isExpanded;
private bool? _isEnabled = false;
+ private bool _isVisible;
- protected EventTreeNodeBase(EventTreeNodeBase parent, string text)
+ protected EventTreeNodeBase(EventTreeNodeBase? parent, string text)
{
Parent = parent;
Text = text;
+ IsVisible = true;
}
- public IAvaloniaReadOnlyList Children
+ public IAvaloniaReadOnlyList? Children
{
get;
protected set;
@@ -33,7 +35,13 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _isEnabled, value);
}
- public EventTreeNodeBase Parent
+ public bool IsVisible
+ {
+ get => _isVisible;
+ set => RaiseAndSetIfChanged(ref _isVisible, value);
+ }
+
+ public EventTreeNodeBase? Parent
{
get;
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
index 361e82bc73..fbcedb2e74 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
@@ -1,28 +1,44 @@
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Globalization;
using System.Linq;
using Avalonia.Controls;
-using Avalonia.Data.Converters;
+using Avalonia.Diagnostics.Models;
+using Avalonia.Input;
using Avalonia.Interactivity;
-using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventsPageViewModel : ViewModelBase
{
- private readonly IControl _root;
- private FiredEvent _selectedEvent;
+ private static readonly HashSet s_defaultEvents = new HashSet()
+ {
+ Button.ClickEvent,
+ InputElement.KeyDownEvent,
+ InputElement.KeyUpEvent,
+ InputElement.TextInputEvent,
+ InputElement.PointerReleasedEvent,
+ InputElement.PointerPressedEvent
+ };
+
+ private readonly MainViewModel _mainViewModel;
+ private FiredEvent? _selectedEvent;
+ private EventTreeNodeBase? _selectedNode;
- public EventsPageViewModel(IControl root)
+ public EventsPageViewModel(MainViewModel mainViewModel)
{
- _root = root;
+ _mainViewModel = mainViewModel;
Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
.GroupBy(e => e.OwnerType)
.OrderBy(e => e.Key.Name)
.Select(g => new EventOwnerTreeNode(g.Key, g, this))
.ToArray();
+
+ EventsFilter = new FilterViewModel();
+ EventsFilter.RefreshFilter += (s, e) => UpdateEventFilters();
+
+ EnableDefault();
}
public string Name => "Events";
@@ -31,15 +47,129 @@ namespace Avalonia.Diagnostics.ViewModels
public ObservableCollection RecordedEvents { get; } = new ObservableCollection();
- public FiredEvent SelectedEvent
+ public FiredEvent? SelectedEvent
{
get => _selectedEvent;
set => RaiseAndSetIfChanged(ref _selectedEvent, value);
}
- private void Clear()
+ public EventTreeNodeBase? SelectedNode
+ {
+ get => _selectedNode;
+ set => RaiseAndSetIfChanged(ref _selectedNode, value);
+ }
+
+ public FilterViewModel EventsFilter { get; }
+
+ public void Clear()
{
RecordedEvents.Clear();
}
+
+ public void DisableAll()
+ {
+ EvaluateNodeEnabled(_ => false);
+ }
+
+ public void EnableDefault()
+ {
+ EvaluateNodeEnabled(node => s_defaultEvents.Contains(node.Event));
+ }
+
+ public void RequestTreeNavigateTo(EventChainLink navTarget)
+ {
+ if (navTarget.Handler is IControl control)
+ {
+ _mainViewModel.RequestTreeNavigateTo(control, true);
+ }
+ }
+
+ public void SelectEventByType(RoutedEvent evt)
+ {
+ foreach (var node in Nodes)
+ {
+ var result = FindNode(node, evt);
+
+ if (result != null && result.IsVisible)
+ {
+ SelectedNode = result;
+
+ break;
+ }
+ }
+
+ static EventTreeNodeBase? FindNode(EventTreeNodeBase node, RoutedEvent eventType)
+ {
+ if (node is EventTreeNode eventNode && eventNode.Event == eventType)
+ {
+ return node;
+ }
+
+ if (node.Children != null)
+ {
+ foreach (var child in node.Children)
+ {
+ var result = FindNode(child, eventType);
+
+ if (result != null)
+ {
+ return result;
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+
+ private void EvaluateNodeEnabled(Func eval)
+ {
+ void ProcessNode(EventTreeNodeBase node)
+ {
+ if (node is EventTreeNode eventNode)
+ {
+ node.IsEnabled = eval(eventNode);
+ }
+
+ if (node.Children != null)
+ {
+ foreach (var childNode in node.Children)
+ {
+ ProcessNode(childNode);
+ }
+ }
+ }
+
+ foreach (var node in Nodes)
+ {
+ ProcessNode(node);
+ }
+ }
+
+ private void UpdateEventFilters()
+ {
+ foreach (var node in Nodes)
+ {
+ FilterNode(node, false);
+ }
+
+ bool FilterNode(EventTreeNodeBase node, bool isParentVisible)
+ {
+ bool matchesFilter = EventsFilter.Filter(node.Text);
+ bool hasVisibleChild = false;
+
+ if (node.Children != null)
+ {
+ foreach (var childNode in node.Children)
+ {
+ hasVisibleChild |= FilterNode(childNode, matchesFilter);
+ }
+ }
+
+ node.IsVisible = hasVisibleChild || matchesFilter || isParentVisible;
+
+ return node.IsVisible;
+ }
+ }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs
new file mode 100644
index 0000000000..5b27236f2e
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Text.RegularExpressions;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class FilterViewModel : ViewModelBase, INotifyDataErrorInfo
+ {
+ private readonly Dictionary _errors = new Dictionary();
+ private string _filterString = string.Empty;
+ private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
+ private Regex? _filterRegex;
+
+ public event EventHandler? RefreshFilter;
+
+ public bool HasErrors => _errors.Count > 0;
+
+ public event EventHandler? ErrorsChanged;
+
+ public bool Filter(string input)
+ {
+ return _filterRegex?.IsMatch(input) ?? true;
+ }
+
+ private void UpdateFilterRegex()
+ {
+ void ClearError()
+ {
+ if (_errors.Remove(nameof(FilterString)))
+ {
+ ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(FilterString)));
+ }
+ }
+
+ try
+ {
+ var options = RegexOptions.Compiled;
+ var pattern = UseRegexFilter
+ ? FilterString.Trim() : Regex.Escape(FilterString.Trim());
+ if (!UseCaseSensitiveFilter)
+ {
+ options |= RegexOptions.IgnoreCase;
+ }
+ if (UseWholeWordFilter)
+ {
+ pattern = $"\\b(?:{pattern})\\b";
+ }
+
+ _filterRegex = new Regex(pattern, options);
+ ClearError();
+ }
+ catch (Exception exception)
+ {
+ _errors[nameof(FilterString)] = exception.Message;
+ ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(FilterString)));
+ }
+ }
+
+ public string FilterString
+ {
+ get => _filterString;
+ set
+ {
+ if (RaiseAndSetIfChanged(ref _filterString, value))
+ {
+ UpdateFilterRegex();
+ RefreshFilter?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public bool UseRegexFilter
+ {
+ get => _useRegexFilter;
+ set
+ {
+ if (RaiseAndSetIfChanged(ref _useRegexFilter, value))
+ {
+ UpdateFilterRegex();
+ RefreshFilter?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public bool UseCaseSensitiveFilter
+ {
+ get => _useCaseSensitiveFilter;
+ set
+ {
+ if (RaiseAndSetIfChanged(ref _useCaseSensitiveFilter, value))
+ {
+ UpdateFilterRegex();
+ RefreshFilter?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public bool UseWholeWordFilter
+ {
+ get => _useWholeWordFilter;
+ set
+ {
+ if (RaiseAndSetIfChanged(ref _useWholeWordFilter, value))
+ {
+ UpdateFilterRegex();
+ RefreshFilter?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public IEnumerable GetErrors(string? propertyName)
+ {
+ if (propertyName != null
+ && _errors.TryGetValue(propertyName, out var error))
+ {
+ yield return error;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
index ae53cf6154..32df2f8745 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
@@ -8,15 +8,12 @@ namespace Avalonia.Diagnostics.ViewModels
internal class FiredEvent : ViewModelBase
{
private readonly RoutedEventArgs _eventArgs;
- private EventChainLink _handledBy;
+ private EventChainLink? _handledBy;
public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator)
{
- Contract.Requires(eventArgs != null);
- Contract.Requires(originator != null);
-
- _eventArgs = eventArgs;
- Originator = originator;
+ _eventArgs = eventArgs ?? throw new ArgumentNullException(nameof(eventArgs));
+ Originator = originator ?? throw new ArgumentNullException(nameof(originator));
AddToChain(originator);
}
@@ -25,7 +22,7 @@ namespace Avalonia.Diagnostics.ViewModels
return e == _eventArgs;
}
- public RoutedEvent Event => _eventArgs.RoutedEvent;
+ public RoutedEvent Event => _eventArgs.RoutedEvent!;
public bool IsHandled => HandledBy?.Handled == true;
@@ -38,7 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (IsHandled)
{
return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine +
- $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}";
+ $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy!.HandlerName}";
}
return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}";
@@ -47,7 +44,7 @@ namespace Avalonia.Diagnostics.ViewModels
public EventChainLink Originator { get; }
- public EventChainLink HandledBy
+ public EventChainLink? HandledBy
{
get => _handledBy;
set
@@ -62,13 +59,18 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- public void AddToChain(object handler, bool handled, RoutingStrategies route)
- {
- AddToChain(new EventChainLink(handler, handled, route));
- }
-
public void AddToChain(EventChainLink link)
{
+ if (EventChain.Count > 0)
+ {
+ var prevLink = EventChain[EventChain.Count-1];
+
+ if (prevLink.Route != link.Route)
+ {
+ link.BeginsNewRoute = true;
+ }
+ }
+
EventChain.Add(link);
if (HandledBy == null && link.Handled)
HandledBy = link;
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
index 38788ef8ee..04215fa8ae 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
@@ -7,22 +7,24 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class LogicalTreeNode : TreeNode
{
- public LogicalTreeNode(ILogical logical, TreeNode parent)
+ public LogicalTreeNode(ILogical logical, TreeNode? parent)
: base((Control)logical, parent)
{
Children = new LogicalTreeNodeCollection(this, logical);
}
+ public override TreeNodeCollection Children { get; }
+
public static LogicalTreeNode[] Create(object control)
{
var logical = control as ILogical;
- return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
+ return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty();
}
internal class LogicalTreeNodeCollection : TreeNodeCollection
{
private readonly ILogical _control;
- private IDisposable _subscription;
+ private IDisposable? _subscription;
public LogicalTreeNodeCollection(TreeNode owner, ILogical control)
: base(owner)
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
index 3049431361..3f367165ac 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
+
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
@@ -16,18 +17,21 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly IDisposable _pointerOverSubscription;
private ViewModelBase _content;
private int _selectedTab;
- private string _focusedControl;
- private string _pointerOverElement;
+ private string? _focusedControl;
+ private string? _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay;
+#nullable disable
+ // Remove "nullable disable" after MemberNotNull will work on our CI.
public MainViewModel(TopLevel root)
+#nullable restore
{
_root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root));
- _events = new EventsPageViewModel(root);
+ _events = new EventsPageViewModel(this);
UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged;
@@ -83,6 +87,7 @@ namespace Avalonia.Diagnostics.ViewModels
public ViewModelBase Content
{
get { return _content; }
+ // [MemberNotNull(nameof(_content))]
private set
{
if (_content is TreePageViewModel oldTree &&
@@ -113,39 +118,40 @@ namespace Avalonia.Diagnostics.ViewModels
public int SelectedTab
{
get { return _selectedTab; }
+ // [MemberNotNull(nameof(_content))]
set
{
_selectedTab = value;
switch (value)
{
- case 0:
- Content = _logicalTree;
- break;
case 1:
Content = _visualTree;
break;
case 2:
Content = _events;
break;
+ default:
+ Content = _logicalTree;
+ break;
}
RaisePropertyChanged();
}
}
- public string FocusedControl
+ public string? FocusedControl
{
get { return _focusedControl; }
private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
}
- public string PointerOverElement
+ public string? PointerOverElement
{
get { return _pointerOverElement; }
private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
}
-
+
private void UpdateConsoleContext(ConsoleContext context)
{
context.root = _root;
@@ -186,12 +192,33 @@ namespace Avalonia.Diagnostics.ViewModels
FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
}
- private void KeyboardPropertyChanged(object sender, PropertyChangedEventArgs e)
+ private void KeyboardPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
{
UpdateFocusedControl();
}
}
+
+ public void RequestTreeNavigateTo(IControl control, bool isVisualTree)
+ {
+ var tree = isVisualTree ? _visualTree : _logicalTree;
+
+ var node = tree.FindNode(control);
+
+ if (node != null)
+ {
+ SelectedTab = isVisualTree ? 1 : 0;
+
+ tree.SelectControl(control);
+ }
+ }
+
+ public int? StartupScreenIndex { get; private set; } = default;
+
+ public void SetOptions(DevToolsOptions options)
+ {
+ StartupScreenIndex = options.StartupScreenIndex;
+ }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
index e23d6f1471..fdbd8c1aa3 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
@@ -16,9 +16,11 @@ namespace Avalonia.Diagnostics.ViewModels
public abstract string Group { get; }
public abstract string Type { get; }
public abstract string Value { get; set; }
- public abstract void Update();
+ public abstract string Priority { get; }
+ public abstract bool? IsAttached { get; }
+ public abstract void Update();
- protected static string ConvertToString(object value)
+ protected static string ConvertToString(object? value)
{
if (value is null)
{
@@ -31,13 +33,13 @@ namespace Avalonia.Diagnostics.ViewModels
if (!converter.CanConvertTo(typeof(string)) ||
converter.GetType() == typeof(CollectionConverter))
{
- return value.ToString();
+ return value.ToString() ?? "(null)";
}
return converter.ConvertToString(value);
}
- private static object InvokeParse(string s, Type targetType)
+ private static object? InvokeParse(string s, Type targetType)
{
var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
@@ -56,7 +58,7 @@ namespace Avalonia.Diagnostics.ViewModels
throw new InvalidCastException("Unable to convert value.");
}
- protected static object ConvertFromString(string s, Type targetType)
+ protected static object? ConvertFromString(string s, Type targetType)
{
var converter = TypeDescriptor.GetConverter(targetType);
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
index a82e13fcfa..e93dc7361b 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
@@ -8,7 +8,7 @@ namespace Avalonia.Diagnostics.ViewModels
public IBrush Tint { get; }
- public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object resourceValue, bool isDynamic) : base(property, resourceValue)
+ public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object? resourceValue, bool isDynamic) : base(property, resourceValue)
{
Key = resourceKey;
Tint = isDynamic ? Brushes.Orange : Brushes.Brown;
@@ -16,12 +16,14 @@ namespace Avalonia.Diagnostics.ViewModels
public void CopyResourceKey()
{
- if (Key is null)
+ var textToCopy = Key?.ToString();
+
+ if (textToCopy is null)
{
return;
}
- CopyToClipboard(Key.ToString());
+ CopyToClipboard(textToCopy);
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
index e835f5a878..38cbefcb93 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
@@ -11,7 +11,7 @@ namespace Avalonia.Diagnostics.ViewModels
public string Name { get; }
- public object Value { get; }
+ public object? Value { get; }
public bool IsActive
{
@@ -25,7 +25,7 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _isVisible, value);
}
- public SetterViewModel(AvaloniaProperty property, object value)
+ public SetterViewModel(AvaloniaProperty property, object? value)
{
Property = property;
Name = property.Name;
@@ -36,12 +36,14 @@ namespace Avalonia.Diagnostics.ViewModels
public void CopyValue()
{
- if (Value is null)
+ var textToCopy = Value?.ToString();
+
+ if (textToCopy is null)
{
return;
}
- CopyToClipboard(Value.ToString());
+ CopyToClipboard(textToCopy);
}
public void CopyPropertyName()
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
index 9363c28705..4cb470eeac 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
@@ -9,17 +9,18 @@ using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
- internal class TreeNode : ViewModelBase, IDisposable
+ internal abstract class TreeNode : ViewModelBase, IDisposable
{
- private IDisposable _classesSubscription;
+ private IDisposable? _classesSubscription;
private string _classes;
private bool _isExpanded;
- public TreeNode(IVisual visual, TreeNode parent)
+ public TreeNode(IVisual visual, TreeNode? parent)
{
Parent = parent;
Type = visual.GetType().Name;
Visual = visual;
+ _classes = string.Empty;
if (visual is IControl control)
{
@@ -51,10 +52,9 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- public TreeNodeCollection Children
+ public abstract TreeNodeCollection Children
{
get;
- protected set;
}
public string Classes
@@ -63,7 +63,7 @@ namespace Avalonia.Diagnostics.ViewModels
private set { RaiseAndSetIfChanged(ref _classes, value); }
}
- public string ElementName
+ public string? ElementName
{
get;
}
@@ -79,7 +79,7 @@ namespace Avalonia.Diagnostics.ViewModels
set { RaiseAndSetIfChanged(ref _isExpanded, value); }
}
- public TreeNode Parent
+ public TreeNode? Parent
{
get;
}
@@ -92,7 +92,7 @@ namespace Avalonia.Diagnostics.ViewModels
public void Dispose()
{
- _classesSubscription.Dispose();
+ _classesSubscription?.Dispose();
Children.Dispose();
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
index 8b4f03bd23..c007411f49 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
@@ -3,46 +3,33 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
+
using Avalonia.Collections;
namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList, IDisposable
{
- private AvaloniaList _inner;
+ private AvaloniaList? _inner;
public TreeNodeCollection(TreeNode owner) => Owner = owner;
- public TreeNode this[int index]
- {
- get
- {
- EnsureInitialized();
- return _inner[index];
- }
- }
+ public TreeNode this[int index] => EnsureInitialized()[index];
- public int Count
- {
- get
- {
- EnsureInitialized();
- return _inner.Count;
- }
- }
+ public int Count => EnsureInitialized().Count;
protected TreeNode Owner { get; }
- public event NotifyCollectionChangedEventHandler CollectionChanged
+ public event NotifyCollectionChangedEventHandler? CollectionChanged
{
- add => _inner.CollectionChanged += value;
- remove => _inner.CollectionChanged -= value;
+ add => EnsureInitialized().CollectionChanged += value;
+ remove => EnsureInitialized().CollectionChanged -= value;
}
- public event PropertyChangedEventHandler PropertyChanged
+ public event PropertyChangedEventHandler? PropertyChanged
{
- add => _inner.PropertyChanged += value;
- remove => _inner.PropertyChanged -= value;
+ add => EnsureInitialized().PropertyChanged += value;
+ remove => EnsureInitialized().PropertyChanged -= value;
}
public virtual void Dispose()
@@ -58,21 +45,21 @@ namespace Avalonia.Diagnostics.ViewModels
public IEnumerator GetEnumerator()
{
- EnsureInitialized();
- return _inner.GetEnumerator();
+ return EnsureInitialized().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
protected abstract void Initialize(AvaloniaList nodes);
- private void EnsureInitialized()
+ private AvaloniaList EnsureInitialized()
{
if (_inner is null)
{
_inner = new AvaloniaList();
Initialize(_inner);
}
+ return _inner;
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
index 6b779cd6ac..4b18cf414a 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
@@ -1,53 +1,50 @@
using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Text.RegularExpressions;
using Avalonia.Controls;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
- internal class TreePageViewModel : ViewModelBase, IDisposable, INotifyDataErrorInfo
+ internal class TreePageViewModel : ViewModelBase, IDisposable
{
- private readonly Dictionary _errors = new Dictionary();
- private TreeNode _selectedNode;
- private ControlDetailsViewModel _details;
- private string _propertyFilter = string.Empty;
- private bool _useRegexFilter;
+ private TreeNode? _selectedNode;
+ private ControlDetailsViewModel? _details;
public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes)
{
MainView = mainView;
Nodes = nodes;
+
+ PropertiesFilter = new FilterViewModel();
+ PropertiesFilter.RefreshFilter += (s, e) => Details?.PropertiesView.Refresh();
+
+ SettersFilter = new FilterViewModel();
+ SettersFilter.RefreshFilter += (s, e) => Details?.UpdateStyleFilters();
}
public MainViewModel MainView { get; }
+ public FilterViewModel PropertiesFilter { get; }
+
+ public FilterViewModel SettersFilter { get; }
+
public TreeNode[] Nodes { get; protected set; }
- public TreeNode SelectedNode
+ public TreeNode? SelectedNode
{
get => _selectedNode;
private set
{
- var oldDetails = Details;
-
if (RaiseAndSetIfChanged(ref _selectedNode, value))
{
Details = value != null ?
new ControlDetailsViewModel(this, value.Visual) :
null;
-
- if (Details != null && oldDetails != null)
- {
- Details.StyleFilter = oldDetails.StyleFilter;
- }
+ Details?.UpdateStyleFilters();
}
}
}
- public ControlDetailsViewModel Details
+ public ControlDetailsViewModel? Details
{
get => _details;
private set
@@ -61,63 +58,6 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- public Regex FilterRegex { get; set; }
-
- private void UpdateFilterRegex()
- {
- void ClearError()
- {
- if (_errors.Remove(nameof(PropertyFilter)))
- {
- ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
- }
- }
-
- if (UseRegexFilter)
- {
- try
- {
- FilterRegex = new Regex(PropertyFilter, RegexOptions.Compiled);
- ClearError();
- }
- catch (Exception exception)
- {
- _errors[nameof(PropertyFilter)] = exception.Message;
- ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
- }
- }
- else
- {
- ClearError();
- }
- }
-
- public string PropertyFilter
- {
- get => _propertyFilter;
- set
- {
- if (RaiseAndSetIfChanged(ref _propertyFilter, value))
- {
- UpdateFilterRegex();
- Details.PropertiesView.Refresh();
- }
- }
- }
-
- public bool UseRegexFilter
- {
- get => _useRegexFilter;
- set
- {
- if (RaiseAndSetIfChanged(ref _useRegexFilter, value))
- {
- UpdateFilterRegex();
- Details.PropertiesView.Refresh();
- }
- }
- }
-
public void Dispose()
{
foreach (var node in Nodes)
@@ -128,7 +68,7 @@ namespace Avalonia.Diagnostics.ViewModels
_details?.Dispose();
}
- public TreeNode FindNode(IControl control)
+ public TreeNode? FindNode(IControl control)
{
foreach (var node in Nodes)
{
@@ -164,7 +104,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- private void ExpandNode(TreeNode node)
+ private void ExpandNode(TreeNode? node)
{
if (node != null)
{
@@ -173,7 +113,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- private TreeNode FindNode(TreeNode node, IControl control)
+ private TreeNode? FindNode(TreeNode node, IControl control)
{
if (node.Visual == control)
{
@@ -194,17 +134,5 @@ namespace Avalonia.Diagnostics.ViewModels
return null;
}
-
- public IEnumerable GetErrors(string propertyName)
- {
- if (_errors.TryGetValue(propertyName, out var error))
- {
- yield return error;
- }
- }
-
- public bool HasErrors => _errors.Count > 0;
-
- public event EventHandler ErrorsChanged;
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
index 66e9c34657..a2ee37c625 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
@@ -1,16 +1,16 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ViewModelBase : INotifyPropertyChanged
{
- private PropertyChangedEventHandler _propertyChanged;
+ private PropertyChangedEventHandler? _propertyChanged;
private List events = new List();
- public event PropertyChangedEventHandler PropertyChanged
+ public event PropertyChangedEventHandler? PropertyChanged
{
add { _propertyChanged += value; events.Add("added"); }
remove { _propertyChanged -= value; events.Add("removed"); }
@@ -20,7 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
}
- protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = null)
+ protected bool RaiseAndSetIfChanged([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string propertyName = null!)
{
if (!EqualityComparer.Default.Equals(field, value))
{
@@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.ViewModels
return false;
}
- protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
+ protected void RaisePropertyChanged([CallerMemberName] string propertyName = null!)
{
var e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e);
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
index bc40edf477..48fa636664 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
@@ -7,7 +7,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class VisualTreeNode : TreeNode
{
- public VisualTreeNode(IVisual visual, TreeNode parent)
+ public VisualTreeNode(IVisual visual, TreeNode? parent)
: base(visual, parent)
{
Children = new VisualTreeNodeCollection(this, visual);
@@ -20,16 +20,18 @@ namespace Avalonia.Diagnostics.ViewModels
public bool IsInTemplate { get; private set; }
+ public override TreeNodeCollection Children { get; }
+
public static VisualTreeNode[] Create(object control)
{
var visual = control as IVisual;
- return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
+ return visual != null ? new[] { new VisualTreeNode(visual, null) } : Array.Empty();
}
internal class VisualTreeNodeCollection : TreeNodeCollection
{
private readonly IVisual _control;
- private IDisposable _subscription;
+ private IDisposable? _subscription;
public VisualTreeNodeCollection(TreeNode owner, IVisual control)
: base(owner)
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
index ae70b59fde..ab523fb75a 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
@@ -30,22 +30,26 @@ namespace Avalonia.Diagnostics.Views
AvaloniaXamlLoader.Load(this);
}
- private void HistoryChanged(object sender, NotifyCollectionChangedEventArgs e)
+ private void HistoryChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
- if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems[0] is IControl control)
+ if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is IControl control)
{
DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero);
}
}
- private void InputKeyDown(object sender, KeyEventArgs e)
+ private void InputKeyDown(object? sender, KeyEventArgs e)
{
- var vm = (ConsoleViewModel)DataContext;
+ var vm = (ConsoleViewModel?)DataContext;
+ if (vm is null)
+ {
+ return;
+ }
switch (e.Key)
{
case Key.Enter:
- vm.Execute();
+ _ = vm.Execute();
e.Handled = true;
break;
case Key.Up:
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
index 9ba576c826..4b37438993 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
@@ -2,90 +2,30 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
xmlns:local="clr-namespace:Avalonia.Diagnostics.Views"
+ xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
x:Name="Main">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
+
+
-
+
-
-
-
-
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
- (
-
-
- )
+
+
+
+
-
-
-
+
-
-
-
-
-
-
-
-
+
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (
+
+
+ )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
index c9568509f6..c6bd5a18aa 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
@@ -1,24 +1,10 @@
-using System;
using Avalonia.Controls;
-using Avalonia.Controls.Shapes;
using Avalonia.Markup.Xaml;
-using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.Views
{
internal class ControlDetailsView : UserControl
{
- private ThicknessEditor _borderArea;
- private ThicknessEditor _paddingArea;
- private Rectangle _horizontalSizeBegin;
- private Rectangle _horizontalSizeEnd;
- private Rectangle _verticalSizeBegin;
- private Rectangle _verticalSizeEnd;
- private Grid _layoutRoot;
- private Border _horizontalSize;
- private Border _verticalSize;
- private Border _contentArea;
-
public ControlDetailsView()
{
InitializeComponent();
@@ -27,101 +13,6 @@ namespace Avalonia.Diagnostics.Views
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
-
- _borderArea = this.FindControl("BorderArea");
- _paddingArea = this.FindControl("PaddingArea");
-
- _horizontalSizeBegin = this.FindControl("HorizontalSizeBegin");
- _horizontalSizeEnd = this.FindControl("HorizontalSizeEnd");
- _verticalSizeBegin = this.FindControl("VerticalSizeBegin");
- _verticalSizeEnd = this.FindControl("VerticalSizeEnd");
-
- _horizontalSize = this.FindControl("HorizontalSize");
- _verticalSize = this.FindControl("VerticalSize");
-
- _contentArea = this.FindControl("ContentArea");
-
- _layoutRoot = this.FindControl("LayoutRoot");
-
- void SubscribeToBounds(Visual visual)
- {
- visual.GetPropertyChangedObservable(TransformedBoundsProperty)
- .Subscribe(UpdateSizeGuidelines);
- }
-
- SubscribeToBounds(_borderArea);
- SubscribeToBounds(_paddingArea);
- SubscribeToBounds(_contentArea);
- }
-
- private void UpdateSizeGuidelines(AvaloniaPropertyChangedEventArgs e)
- {
- void UpdateGuidelines(Visual area)
- {
- if (area.TransformedBounds is TransformedBounds bounds)
- {
- // Horizontal guideline
- {
- var sizeArea = TranslateToRoot((_horizontalSize.TransformedBounds ?? default).Bounds.BottomLeft,
- _horizontalSize);
-
- var start = TranslateToRoot(bounds.Bounds.BottomLeft, area);
-
- SetPosition(_horizontalSizeBegin, start);
-
- var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
-
- SetPosition(_horizontalSizeEnd, end.WithX(end.X - 1));
-
- var height = sizeArea.Y - start.Y + 2;
-
- _horizontalSizeBegin.Height = height;
- _horizontalSizeEnd.Height = height;
- }
-
- // Vertical guideline
- {
- var sizeArea = TranslateToRoot((_verticalSize.TransformedBounds ?? default).Bounds.TopRight, _verticalSize);
-
- var start = TranslateToRoot(bounds.Bounds.TopRight, area);
-
- SetPosition(_verticalSizeBegin, start);
-
- var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
-
- SetPosition(_verticalSizeEnd, end.WithY(end.Y - 1));
-
- var width = sizeArea.X - start.X + 2;
-
- _verticalSizeBegin.Width = width;
- _verticalSizeEnd.Width = width;
- }
- }
- }
-
- Point TranslateToRoot(Point point, IVisual from)
- {
- return from.TranslatePoint(point, _layoutRoot) ?? default;
- }
-
- static void SetPosition(Rectangle rect, Point start)
- {
- Canvas.SetLeft(rect, start.X);
- Canvas.SetTop(rect, start.Y);
- }
-
- if (_borderArea.IsPresent)
- {
- UpdateGuidelines(_borderArea);
- }
- else if (_paddingArea.IsPresent)
- {
- UpdateGuidelines(_paddingArea);
- }
- else
- {
- UpdateGuidelines(_contentArea);
- }
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
index b7f0860e70..a9c2688a18 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
@@ -2,58 +2,136 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
- x:Class="Avalonia.Diagnostics.Views.EventsPageView">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls"
+ x:Class="Avalonia.Diagnostics.Views.EventsPageView"
+ Margin="2">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
index c1c78d38f6..ba7ab41e35 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
@@ -1,7 +1,14 @@
-using System.Linq;
+using System;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Linq;
using Avalonia.Controls;
+using Avalonia.Diagnostics.Models;
using Avalonia.Diagnostics.ViewModels;
+using Avalonia.Input;
+using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
namespace Avalonia.Diagnostics.Views
{
@@ -12,13 +19,53 @@ namespace Avalonia.Diagnostics.Views
public EventsPageView()
{
InitializeComponent();
- _events = this.FindControl("events");
+ _events = this.FindControl("EventsList");
}
- private void RecordedEvents_CollectionChanged(object sender,
- System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+ public void NavigateTo(object sender, TappedEventArgs e)
{
- _events.ScrollIntoView(_events.Items.OfType().LastOrDefault());
+ if (DataContext is EventsPageViewModel vm && sender is Control control)
+ {
+ switch (control.Tag)
+ {
+ case EventChainLink chainLink:
+ {
+ vm.RequestTreeNavigateTo(chainLink);
+ break;
+ }
+ case RoutedEvent evt:
+ {
+ vm.SelectEventByType(evt);
+
+ break;
+ }
+ }
+ }
+ }
+
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+
+ if (DataContext is EventsPageViewModel vm)
+ {
+ vm.RecordedEvents.CollectionChanged += OnRecordedEventsChanged;
+ }
+ }
+
+ private void OnRecordedEventsChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (sender is ObservableCollection events)
+ {
+ var evt = events.LastOrDefault();
+
+ if (evt is null)
+ {
+ return;
+ }
+
+ Dispatcher.UIThread.Post(() => _events.ScrollIntoView(evt));
+ }
}
private void InitializeComponent()
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml
new file mode 100644
index 0000000000..af6c84a76a
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs
new file mode 100644
index 0000000000..d4a09f7296
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs
@@ -0,0 +1,128 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Diagnostics.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Diagnostics.Views
+{
+ internal class LayoutExplorerView : UserControl
+ {
+ private readonly ThicknessEditor _borderArea;
+ private readonly ThicknessEditor _paddingArea;
+ private readonly Rectangle _horizontalSizeBegin;
+ private readonly Rectangle _horizontalSizeEnd;
+ private readonly Rectangle _verticalSizeBegin;
+ private readonly Rectangle _verticalSizeEnd;
+ private readonly Grid _layoutRoot;
+ private readonly Border _horizontalSize;
+ private readonly Border _verticalSize;
+ private readonly Border _contentArea;
+
+ public LayoutExplorerView()
+ {
+ InitializeComponent();
+
+ _borderArea = this.FindControl("BorderArea");
+ _paddingArea = this.FindControl("PaddingArea");
+
+ _horizontalSizeBegin = this.FindControl("HorizontalSizeBegin");
+ _horizontalSizeEnd = this.FindControl("HorizontalSizeEnd");
+ _verticalSizeBegin = this.FindControl("VerticalSizeBegin");
+ _verticalSizeEnd = this.FindControl("VerticalSizeEnd");
+
+ _horizontalSize = this.FindControl("HorizontalSize");
+ _verticalSize = this.FindControl("VerticalSize");
+
+ _contentArea = this.FindControl("ContentArea");
+
+ _layoutRoot = this.FindControl("LayoutRoot");
+
+ void SubscribeToBounds(Visual visual)
+ {
+ visual.GetPropertyChangedObservable(TransformedBoundsProperty)
+ .Subscribe(UpdateSizeGuidelines);
+ }
+
+ SubscribeToBounds(_borderArea);
+ SubscribeToBounds(_paddingArea);
+ SubscribeToBounds(_contentArea);
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void UpdateSizeGuidelines(AvaloniaPropertyChangedEventArgs e)
+ {
+ void UpdateGuidelines(Visual area)
+ {
+ if (area.TransformedBounds is TransformedBounds bounds)
+ {
+ // Horizontal guideline
+ {
+ var sizeArea = TranslateToRoot((_horizontalSize.TransformedBounds ?? default).Bounds.BottomLeft,
+ _horizontalSize);
+
+ var start = TranslateToRoot(bounds.Bounds.BottomLeft, area);
+
+ SetPosition(_horizontalSizeBegin, start);
+
+ var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
+
+ SetPosition(_horizontalSizeEnd, end.WithX(end.X - 1));
+
+ var height = sizeArea.Y - start.Y + 2;
+
+ _horizontalSizeBegin.Height = height;
+ _horizontalSizeEnd.Height = height;
+ }
+
+ // Vertical guideline
+ {
+ var sizeArea = TranslateToRoot((_verticalSize.TransformedBounds ?? default).Bounds.TopRight, _verticalSize);
+
+ var start = TranslateToRoot(bounds.Bounds.TopRight, area);
+
+ SetPosition(_verticalSizeBegin, start);
+
+ var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
+
+ SetPosition(_verticalSizeEnd, end.WithY(end.Y - 1));
+
+ var width = sizeArea.X - start.X + 2;
+
+ _verticalSizeBegin.Width = width;
+ _verticalSizeEnd.Width = width;
+ }
+ }
+ }
+
+ Point TranslateToRoot(Point point, IVisual from)
+ {
+ return from.TranslatePoint(point, _layoutRoot) ?? default;
+ }
+
+ static void SetPosition(Rectangle rect, Point start)
+ {
+ Canvas.SetLeft(rect, start.X);
+ Canvas.SetTop(rect, start.Y);
+ }
+
+ if (_borderArea.IsPresent)
+ {
+ UpdateGuidelines(_borderArea);
+ }
+ else if (_paddingArea.IsPresent)
+ {
+ UpdateGuidelines(_paddingArea);
+ }
+ else
+ {
+ UpdateGuidelines(_contentArea);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
index 783709e54b..b688ad7676 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
@@ -27,7 +27,11 @@ namespace Avalonia.Diagnostics.Views
public void ToggleConsole()
{
- var vm = (MainViewModel)DataContext;
+ var vm = (MainViewModel?)DataContext;
+ if (vm is null)
+ {
+ return;
+ }
if (_consoleHeight == -1)
{
@@ -54,7 +58,7 @@ namespace Avalonia.Diagnostics.Views
AvaloniaXamlLoader.Load(this);
}
- private void PreviewKeyDown(object sender, KeyEventArgs e)
+ private void PreviewKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
index 7dd4ed0832..70d70f0b79 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
@@ -15,6 +15,8 @@
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
index 330121321a..d1232b749a 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
@@ -15,7 +15,7 @@ namespace Avalonia.Diagnostics.Views
internal class MainWindow : Window, IStyleHost
{
private readonly IDisposable _keySubscription;
- private TopLevel _root;
+ private TopLevel? _root;
public MainWindow()
{
@@ -24,9 +24,26 @@ namespace Avalonia.Diagnostics.Views
_keySubscription = InputManager.Instance.Process
.OfType()
.Subscribe(RawKeyDown);
+
+ EventHandler? lh = default;
+ lh = (s, e) =>
+ {
+ this.Opened -= lh;
+ if ((DataContext as MainViewModel)?.StartupScreenIndex is int index)
+ {
+ var screens = this.Screens;
+ if (index > -1 && index < screens.ScreenCount)
+ {
+ var screen = screens.All[index];
+ this.Position = screen.Bounds.TopLeft;
+ this.WindowState = WindowState.Maximized;
+ }
+ }
+ };
+ this.Opened += lh;
}
- public TopLevel Root
+ public TopLevel? Root
{
get => _root;
set
@@ -43,7 +60,7 @@ namespace Avalonia.Diagnostics.Views
if (_root != null)
{
_root.Closed += RootClosed;
- DataContext = new MainViewModel(value);
+ DataContext = new MainViewModel(_root);
}
else
{
@@ -53,15 +70,20 @@ namespace Avalonia.Diagnostics.Views
}
}
- IStyleHost IStyleHost.StylingParent => null;
+ IStyleHost? IStyleHost.StylingParent => null;
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
_keySubscription.Dispose();
- _root.Closed -= RootClosed;
- _root = null;
- ((MainViewModel)DataContext)?.Dispose();
+
+ if (_root != null)
+ {
+ _root.Closed -= RootClosed;
+ _root = null;
+ }
+
+ ((MainViewModel?)DataContext)?.Dispose();
}
private void InitializeComponent()
@@ -71,12 +93,20 @@ namespace Avalonia.Diagnostics.Views
private void RawKeyDown(RawKeyEventArgs e)
{
+ var vm = (MainViewModel?)DataContext;
+ if (vm is null)
+ {
+ return;
+ }
+
const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift;
if (e.Modifiers == modifiers)
{
+#pragma warning disable CS0618 // Type or member is obsolete
var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default;
-
+#pragma warning restore CS0618 // Type or member is obsolete
+
var control = Root.GetVisualsAt(point, x =>
{
if (x is AdornerLayer || !x.IsVisible) return false;
@@ -87,7 +117,6 @@ namespace Avalonia.Diagnostics.Views
if (control != null)
{
- var vm = (MainViewModel)DataContext;
vm.SelectControl((IControl)control);
}
}
@@ -97,12 +126,14 @@ namespace Avalonia.Diagnostics.Views
{
var enable = e.Key == Key.S;
- var vm = (MainViewModel)DataContext;
vm.EnableSnapshotStyles(enable);
}
}
}
- private void RootClosed(object sender, EventArgs e) => Close();
+ private void RootClosed(object? sender, EventArgs e) => Close();
+
+ public void SetOptions(DevToolsOptions options) =>
+ (DataContext as MainViewModel)?.SetOptions(options);
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
index 86137dfc57..a5328716fc 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
@@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.TreePageView">
-
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
index 1b61986ce6..3543b1adea 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Generators;
@@ -6,18 +7,20 @@ using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
+using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.Views
{
internal class TreePageView : UserControl
{
private readonly Panel _adorner;
- private AdornerLayer _currentLayer;
+ private AdornerLayer? _currentLayer;
private TreeView _tree;
public TreePageView()
{
InitializeComponent();
+ _tree = this.FindControl("tree");
_tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
_adorner = new Panel
@@ -35,9 +38,15 @@ namespace Avalonia.Diagnostics.Views
};
}
- protected void AddAdorner(object sender, PointerEventArgs e)
+ protected void AddAdorner(object? sender, PointerEventArgs e)
{
- var node = (TreeNode)((Control)sender).DataContext;
+ var node = (TreeNode?)((Control)sender!).DataContext;
+ var vm = (TreePageViewModel?)DataContext;
+ if (node is null || vm is null)
+ {
+ return;
+ }
+
var visual = (Visual)node.Visual;
_currentLayer = AdornerLayer.GetAdornerLayer(visual);
@@ -51,8 +60,6 @@ namespace Avalonia.Diagnostics.Views
_currentLayer.Children.Add(_adorner);
AdornerLayer.SetAdornedElement(_adorner, visual);
- var vm = (TreePageViewModel) DataContext;
-
if (vm.MainView.ShouldVisualizeMarginPadding)
{
var paddingBorder = (Border)_adorner.Children[0];
@@ -72,7 +79,7 @@ namespace Avalonia.Diagnostics.Views
return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom);
}
- protected void RemoveAdorner(object sender, PointerEventArgs e)
+ protected void RemoveAdorner(object? sender, PointerEventArgs e)
{
foreach (var border in _adorner.Children.OfType())
{
@@ -88,24 +95,30 @@ namespace Avalonia.Diagnostics.Views
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
- _tree = this.FindControl("tree");
}
- private void TreeViewItemMaterialized(object sender, ItemContainerEventArgs e)
+ private void TreeViewItemMaterialized(object? sender, ItemContainerEventArgs e)
{
var item = (TreeViewItem)e.Containers[0].ContainerControl;
item.TemplateApplied += TreeViewItemTemplateApplied;
}
- private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e)
+ private void TreeViewItemTemplateApplied(object? sender, TemplateAppliedEventArgs e)
{
- var item = (TreeViewItem)sender;
- var headerPresenter = item.HeaderPresenter;
- headerPresenter.ApplyTemplate();
+ var item = (TreeViewItem)sender!;
+
+ // This depends on the default tree item template.
+ // We want to handle events in the item header but exclude events coming from children.
+ var header = item.FindDescendantOfType();
+
+ Debug.Assert(header != null);
+
+ if (header != null)
+ {
+ header.PointerEnter += AddAdorner;
+ header.PointerLeave += RemoveAdorner;
+ }
- var header = headerPresenter.Child;
- header.PointerEnter += AddAdorner;
- header.PointerLeave += RemoveAdorner;
item.TemplateApplied -= TreeViewItemTemplateApplied;
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs
index 6f699339e7..4adcd32302 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Diagnostics
private static void PrintVisualTree(IVisual visual, StringBuilder builder, int indent)
{
- Control control = visual as Control;
+ Control? control = visual as Control;
builder.Append(Indent(indent - 1));
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index addc89b205..268171d467 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -70,6 +70,28 @@ namespace Avalonia.Headless
return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
}
+ public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(string fileName)
+ {
+ return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream)
+ {
+ return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
+ }
+
public IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride)
{
return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs
index 7aa9c32bca..2245ff9986 100644
--- a/src/Avalonia.Input/IInputElement.cs
+++ b/src/Avalonia.Input/IInputElement.cs
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
+#nullable enable
+
namespace Avalonia.Input
{
///
diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs
index 8f99770b3b..65b9acae76 100644
--- a/src/Avalonia.Input/InputElement.cs
+++ b/src/Avalonia.Input/InputElement.cs
@@ -9,6 +9,8 @@ using Avalonia.Input.TextInput;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
+#nullable enable
+
namespace Avalonia.Input
{
///
diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs
index 5899824c29..a159b19026 100644
--- a/src/Avalonia.Input/KeyboardDevice.cs
+++ b/src/Avalonia.Input/KeyboardDevice.cs
@@ -60,7 +60,7 @@ namespace Avalonia.Input
ie.IsKeyboardFocusWithin = false;
}
- el = (IInputElement)el.VisualParent;
+ el = (IInputElement?)el.VisualParent;
}
}
@@ -212,17 +212,36 @@ namespace Avalonia.Input
Source = element,
};
- IVisual currentHandler = element;
+ IVisual? currentHandler = element;
while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown)
{
var bindings = (currentHandler as IInputElement)?.KeyBindings;
if (bindings != null)
+ {
+ KeyBinding[]? bindingsCopy = null;
+
+ // Create a copy of the KeyBindings list if there's a binding which matches the event.
+ // If we don't do this the foreach loop will throw an InvalidOperationException when the KeyBindings list is changed.
+ // This can happen when a new view is loaded which adds its own KeyBindings to the handler.
foreach (var binding in bindings)
{
- if (ev.Handled)
+ if (binding.Gesture?.Matches(ev) == true)
+ {
+ bindingsCopy = bindings.ToArray();
break;
- binding.TryHandle(ev);
+ }
+ }
+
+ if (bindingsCopy is object)
+ {
+ foreach (var binding in bindingsCopy)
+ {
+ if (ev.Handled)
+ break;
+ binding.TryHandle(ev);
+ }
}
+ }
currentHandler = currentHandler.VisualParent;
}
diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs
index 6e937b7e13..cfa3690daf 100644
--- a/src/Avalonia.Input/MouseDevice.cs
+++ b/src/Avalonia.Input/MouseDevice.cs
@@ -377,7 +377,7 @@ namespace Avalonia.Input
e.Source = element;
e.Handled = false;
element.RaiseEvent(e);
- element = (IInputElement)element.VisualParent;
+ element = (IInputElement?)element.VisualParent;
}
root.PointerOverElement = null;
@@ -444,7 +444,7 @@ namespace Avalonia.Input
branch = el;
break;
}
- el = (IInputElement)el.VisualParent;
+ el = (IInputElement?)el.VisualParent;
}
el = root.PointerOverElement;
@@ -460,7 +460,7 @@ namespace Avalonia.Input
e.Source = el;
e.Handled = false;
el.RaiseEvent(e);
- el = (IInputElement)el.VisualParent;
+ el = (IInputElement?)el.VisualParent;
}
el = root.PointerOverElement = element;
@@ -471,7 +471,7 @@ namespace Avalonia.Input
e.Source = el;
e.Handled = false;
el.RaiseEvent(e);
- el = (IInputElement)el.VisualParent;
+ el = (IInputElement?)el.VisualParent;
}
}
diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs
index 5c01f870ab..afda29e329 100644
--- a/src/Avalonia.Interactivity/IInteractive.cs
+++ b/src/Avalonia.Interactivity/IInteractive.cs
@@ -1,5 +1,7 @@
using System;
+#nullable enable
+
namespace Avalonia.Interactivity
{
///
diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs
index 580704bb19..4cd810af20 100644
--- a/src/Avalonia.Interactivity/Interactive.cs
+++ b/src/Avalonia.Interactivity/Interactive.cs
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using Avalonia.Layout;
using Avalonia.VisualTree;
+#nullable enable
+
namespace Avalonia.Interactivity
{
///
diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs
index cb13deb15f..3f106708e6 100644
--- a/src/Avalonia.Layout/ElementManager.cs
+++ b/src/Avalonia.Layout/ElementManager.cs
@@ -325,7 +325,10 @@ namespace Avalonia.Layout
break;
case NotifyCollectionChangedAction.Move:
- throw new NotImplementedException();
+ int size = args.OldItems != null ? args.OldItems.Count : 1;
+ OnItemsRemoved(args.OldStartingIndex, size);
+ OnItemsAdded(args.NewStartingIndex, size);
+ break;
}
}
}
diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs
index a1d00017ed..7568ea8e09 100644
--- a/src/Avalonia.Layout/Layoutable.cs
+++ b/src/Avalonia.Layout/Layoutable.cs
@@ -782,7 +782,7 @@ namespace Avalonia.Layout
}
///
- protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
+ protected sealed override void OnVisualParentChanged(IVisual? oldParent, IVisual? newParent)
{
LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
diff --git a/src/Avalonia.Layout/WrapLayout/WrapLayout.cs b/src/Avalonia.Layout/WrapLayout/WrapLayout.cs
index dccbb2205e..ded2afc3dd 100644
--- a/src/Avalonia.Layout/WrapLayout/WrapLayout.cs
+++ b/src/Avalonia.Layout/WrapLayout/WrapLayout.cs
@@ -236,6 +236,15 @@ namespace Avalonia.Layout
// for the last condition it is zeros so adding it will make no difference
// this way is faster than an if condition in every loop for checking the last item
totalMeasure.U = parentMeasure.U;
+
+ // Propagating an infinite size causes a crash. This can happen if the parent is scrollable and infinite in the opposite
+ // axis to the panel. Clearing to zero prevents the crash.
+ // This is likely an incorrect use of the control by the developer, however we need stability here so setting a default that wont crash.
+ if (double.IsInfinity(totalMeasure.U))
+ {
+ totalMeasure.U = 0.0;
+ }
+
totalMeasure.V = state.GetHeight();
totalMeasure.U = Math.Ceiling(totalMeasure.U);
diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs
index 71359f733d..f716464d14 100644
--- a/src/Avalonia.Native/WindowImplBase.cs
+++ b/src/Avalonia.Native/WindowImplBase.cs
@@ -414,9 +414,7 @@ namespace Avalonia.Native
public Action Input { get; set; }
- Action ScalingChanged { get; set; }
-
- Action ITopLevelImpl.ScalingChanged { get; set; }
+ public Action ScalingChanged { get; set; }
public Action TransparencyLevelChanged { get; set; }
diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs
index 9685ecbe91..c4dd79f468 100644
--- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs
+++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs
@@ -1,4 +1,6 @@
using System;
+using System.Threading;
+
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Styling;
@@ -22,7 +24,9 @@ namespace Avalonia.ReactiveUI
///
public static readonly StyledProperty DefaultContentProperty =
AvaloniaProperty.Register(nameof(DefaultContent));
-
+
+ private CancellationTokenSource? _lastTransitionCts;
+
///
/// Gets or sets the animation played when content appears and disappears.
///
@@ -62,11 +66,14 @@ namespace Avalonia.ReactiveUI
/// New content to set.
private async void UpdateContentWithTransition(object? content)
{
+ _lastTransitionCts?.Cancel();
+ _lastTransitionCts = new CancellationTokenSource();
+
if (PageTransition != null)
- await PageTransition.Start(this, null, true);
+ await PageTransition.Start(this, null, true, _lastTransitionCts.Token);
base.Content = content;
if (PageTransition != null)
- await PageTransition.Start(null, this, true);
+ await PageTransition.Start(null, this, true, _lastTransitionCts.Token);
}
}
}
diff --git a/src/Avalonia.Styling/Controls/Classes.cs b/src/Avalonia.Styling/Controls/Classes.cs
index 4e2783d4ec..50605661fa 100644
--- a/src/Avalonia.Styling/Controls/Classes.cs
+++ b/src/Avalonia.Styling/Controls/Classes.cs
@@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using Avalonia.Collections;
+#nullable enable
+
namespace Avalonia.Controls
{
///
@@ -90,7 +92,7 @@ namespace Avalonia.Controls
}
///
- /// Remvoes all non-pseudoclasses from the collection.
+ /// Removes all non-pseudoclasses from the collection.
///
public override void Clear()
{
@@ -135,7 +137,7 @@ namespace Avalonia.Controls
///
public override void InsertRange(int index, IEnumerable names)
{
- var c = new List();
+ List? toInsert = null;
foreach (var name in names)
{
@@ -143,11 +145,16 @@ namespace Avalonia.Controls
if (!Contains(name))
{
- c.Add(name);
+ toInsert ??= new List();
+
+ toInsert.Add(name);
}
}
- base.InsertRange(index, c);
+ if (toInsert != null)
+ {
+ base.InsertRange(index, toInsert);
+ }
}
///
@@ -176,19 +183,21 @@ namespace Avalonia.Controls
///
public override void RemoveAll(IEnumerable names)
{
- var c = new List();
+ List? toRemove = null;
foreach (var name in names)
{
ThrowIfPseudoclass(name, "removed");
- if (Contains(name))
- {
- c.Add(name);
- }
+ toRemove ??= new List();
+
+ toRemove.Add(name);
}
- base.RemoveAll(c);
+ if (toRemove != null)
+ {
+ base.RemoveAll(toRemove);
+ }
}
///
@@ -223,7 +232,7 @@ namespace Avalonia.Controls
/// The new contents of the collection.
public void Replace(IList source)
{
- var toRemove = new List();
+ List? toRemove = null;
foreach (var name in source)
{
@@ -234,11 +243,17 @@ namespace Avalonia.Controls
{
if (!name.StartsWith(":"))
{
+ toRemove ??= new List();
+
toRemove.Add(name);
}
}
- base.RemoveAll(toRemove);
+ if (toRemove != null)
+ {
+ base.RemoveAll(toRemove);
+ }
+
base.AddRange(source);
}
diff --git a/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs b/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs
index ef3c1d5b89..dbf8c68892 100644
--- a/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs
+++ b/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
namespace Avalonia.Controls
{
///
@@ -14,6 +16,6 @@ namespace Avalonia.Controls
/// Sets the control's inheritance parent.
///
/// The parent.
- void SetParent(IAvaloniaObject parent);
+ void SetParent(IAvaloniaObject? parent);
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Styling/Controls/ISetLogicalParent.cs b/src/Avalonia.Styling/Controls/ISetLogicalParent.cs
index 0c0cd1c1bb..85bda05961 100644
--- a/src/Avalonia.Styling/Controls/ISetLogicalParent.cs
+++ b/src/Avalonia.Styling/Controls/ISetLogicalParent.cs
@@ -1,5 +1,7 @@
using Avalonia.LogicalTree;
+#nullable enable
+
namespace Avalonia.Controls
{
///
@@ -14,6 +16,6 @@ namespace Avalonia.Controls
/// Sets the control's parent.
///
/// The parent.
- void SetParent(ILogical parent);
+ void SetParent(ILogical? parent);
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Styling/IDataContextProvider.cs b/src/Avalonia.Styling/IDataContextProvider.cs
index 31639c5784..1172adcaa4 100644
--- a/src/Avalonia.Styling/IDataContextProvider.cs
+++ b/src/Avalonia.Styling/IDataContextProvider.cs
@@ -1,4 +1,6 @@
-namespace Avalonia
+#nullable enable
+
+namespace Avalonia
{
///
/// Defines an element with a data context that can be used for binding.
@@ -8,6 +10,6 @@
///
/// Gets or sets the element's data context.
///
- object DataContext { get; set; }
+ object? DataContext { get; set; }
}
}
diff --git a/src/Avalonia.Styling/IStyledElement.cs b/src/Avalonia.Styling/IStyledElement.cs
index 046a6f9872..a068d4a5bf 100644
--- a/src/Avalonia.Styling/IStyledElement.cs
+++ b/src/Avalonia.Styling/IStyledElement.cs
@@ -4,6 +4,8 @@ using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Styling;
+#nullable enable
+
namespace Avalonia
{
public interface IStyledElement :
@@ -17,7 +19,7 @@ namespace Avalonia
///
/// Occurs when the control has finished initialization.
///
- event EventHandler Initialized;
+ event EventHandler? Initialized;
///
/// Gets a value that indicates whether the element has finished initialization.
@@ -32,6 +34,6 @@ namespace Avalonia
///
/// Gets the control's logical parent.
///
- IStyledElement Parent { get; }
+ IStyledElement? Parent { get; }
}
}
diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs
index 5f26f65512..2292f5c518 100644
--- a/src/Avalonia.Styling/StyledElement.cs
+++ b/src/Avalonia.Styling/StyledElement.cs
@@ -300,7 +300,7 @@ namespace Avalonia
bool IStyleHost.IsStylesInitialized => _styles != null;
///
- IStyleHost? IStyleHost.StylingParent => (IStyleHost)InheritanceParent;
+ IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent;
///
public virtual void BeginInit()
@@ -465,7 +465,7 @@ namespace Avalonia
/// Sets the styled element's inheritance parent.
///
/// The parent.
- void ISetInheritanceParent.SetParent(IAvaloniaObject parent)
+ void ISetInheritanceParent.SetParent(IAvaloniaObject? parent)
{
InheritanceParent = parent;
}
diff --git a/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs
index 9e30e4fa14..8d446ebc9c 100644
--- a/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs
+++ b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs
@@ -33,6 +33,6 @@ namespace Avalonia.Styling.Activators
void IObserver.OnCompleted() { }
void IObserver.OnError(Exception error) { }
- void IObserver.OnNext(object value) => PublishNext(Equals(value, _value));
+ void IObserver.OnNext(object value) => PublishNext(PropertyEqualsSelector.Compare(_property.PropertyType, value, _value));
}
}
diff --git a/src/Avalonia.Styling/Styling/ISetterValue.cs b/src/Avalonia.Styling/Styling/ISetterValue.cs
index 63c544cf7d..0fd245a429 100644
--- a/src/Avalonia.Styling/Styling/ISetterValue.cs
+++ b/src/Avalonia.Styling/Styling/ISetterValue.cs
@@ -1,4 +1,6 @@
-namespace Avalonia.Styling
+#nullable enable
+
+namespace Avalonia.Styling
{
///
/// Customizes the behavior of a class when added as a value to an .
diff --git a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs
index cdd985ac80..5d9c3fe56b 100644
--- a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs
+++ b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs
@@ -1,4 +1,6 @@
using System;
+using System.ComponentModel;
+using System.Globalization;
using System.Text;
using Avalonia.Styling.Activators;
@@ -75,11 +77,37 @@ namespace Avalonia.Styling
}
else
{
- var result = (control.GetValue(_property) ?? string.Empty).Equals(_value);
- return result ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
+ return Compare(_property.PropertyType, control.GetValue(_property), _value)
+ ? SelectorMatch.AlwaysThisInstance
+ : SelectorMatch.NeverThisInstance;
}
+
}
protected override Selector? MovePrevious() => _previous;
+
+ internal static bool Compare(Type propertyType, object propertyValue, object? value)
+ {
+ if (propertyType == typeof(object) &&
+ propertyValue?.GetType() is Type inferredType)
+ {
+ propertyType = inferredType;
+ }
+
+ var valueType = value?.GetType();
+
+ if (valueType is null || propertyType.IsAssignableFrom(valueType))
+ {
+ return Equals(propertyValue, value);
+ }
+
+ var converter = TypeDescriptor.GetConverter(propertyType);
+ if (converter?.CanConvertFrom(valueType) == true)
+ {
+ return Equals(propertyValue, converter.ConvertFrom(null, CultureInfo.InvariantCulture, value));
+ }
+
+ return false;
+ }
}
}
diff --git a/src/Avalonia.Styling/Styling/PropertySetterInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs
index b52ae6b146..1c3055fed6 100644
--- a/src/Avalonia.Styling/Styling/PropertySetterInstance.cs
+++ b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs
@@ -50,7 +50,7 @@ namespace Avalonia.Styling
}
else
{
- _subscription = _target.Bind(_directProperty, this);
+ _subscription = _target.Bind(_directProperty!, this);
}
}
else
@@ -100,7 +100,7 @@ namespace Avalonia.Styling
}
else
{
- _target.ClearValue(_directProperty);
+ _target.ClearValue(_directProperty!);
}
}
diff --git a/src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs
index 03d33faff9..92653d0064 100644
--- a/src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs
+++ b/src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs
@@ -53,7 +53,7 @@ namespace Avalonia.Styling
}
else
{
- _subscription = _target.Bind(_directProperty, this);
+ _subscription = _target.Bind(_directProperty!, this);
}
}
@@ -91,7 +91,7 @@ namespace Avalonia.Styling
}
else
{
- _target.ClearValue(_directProperty);
+ _target.ClearValue(_directProperty!);
}
}
diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml
index e91d8a6772..1ad996a1a0 100644
--- a/src/Avalonia.Themes.Default/ListBox.xaml
+++ b/src/Avalonia.Themes.Default/ListBox.xaml
@@ -13,7 +13,8 @@
+ VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
+ AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
+ VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
+ AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
-
+
diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml
index 0e34eb3f3b..1e2acf736d 100644
--- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml
@@ -1,6 +1,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml
index fb08b97b47..915be08e53 100644
--- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml
@@ -2,16 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
-
-
- #FF0078D7
- #FF005A9E
- #FF004275
- #FF002642
- #FF429CE3
- #FF76B9ED
- #FFA6D8FF
-
#FF000000
#33000000
@@ -40,6 +30,9 @@
#33FFFFFF
#FFF000
+ #18FFFFFF
+ #30FFFFFF
+
@@ -135,7 +128,6 @@
-
@@ -150,197 +142,39 @@
- False
+
+
+
+
+
+
+ #FF000000
+
-
- XamlAutoFontFamily
- XamlAutoFontFamily
- Segoe WP
- Segoe WP SemiLight
- XamlAutoFontFamily
- Segoe MDL2 Assets
- XamlAutoFontFamily
- 24
- 48
- 56
- 24
- 40
- 3
374
- 0
- 2
- 0
- 21
- 64
- 80
- 240
- 14
- 14
- 320
- 548
- 184
- 756
- 130
- 202
- 32
- 32
- 56
- 80
- 20
- 20
- 758
- 456
- 40
- 96
- 240
- 4
- 34
- 20
- 14
- 80
- 16.0
- 42
- 180
- 12
- 48
- 48
- 96
- 16
- 21
- 62
- -112
- 42
- 289
- 96
- 8
- 16
- 0.6
- 4
- 2
- 0
- 14
- 32
- 32
- 270
- 300
- 28
- 4
- 26.667
-
- 4
- 2
- 320
- 48
- 0.8
- 0.8
- 1
- 0.87
- 0.87
- 1
- 0.4
- 0.6
- 1
- 32
- 64
- 18.14
- 25.5
-
- 80
- 12
- 44
- 44
- 20
- 20
-
- 0
- 44
- 44
- 12
- 0,0,0,0
- 0,0,0,0
- 0,0,0,0
- 0,0,0,0
- 3,0,3,0
- 1
0,2,0,2
+ 1
-1,0,-1,0
- 12,11,0,13
- 2
- 2
- 2
- 1
- 0
- 0,4,0,4
- 0,0,0,4
- 2
- 11,5,11,7
- 11,11,11,13
- 11,11,11,13
- 1
- 0,0,4,0
- 0,0,0,0
- 0,0,0,0
- 0,0,0,0
- 0,24,0,0
- 0,0,0,12
- 24,18,24,24
- 0,0,0,4
+ 32
+ 64
+ 456
+ 0
1
0
- 0
- 0,37,0,0
- 0,19,0,0
- 19,0,19,17.5
- 0
- 0,0,0,0
+
12,11,12,12
- 4
- 2.5
- 5,6,5,6
- 0,0,0,9
- 24,0,0,11
- 0
- 0,9.5,0,0
- 0,0,0,19
- 19,19,19,0
- 19,37,19,0
- 0,0,0,32.5
- 0
- 2
- 1
- 12,11,8,13
- 12,11,8,13
- 12,11,8,13
- 0,0,12,0
- 12,4,8,4
- 0,2,0,0
- 2
- 0
- 0,0,0,4
- 0,0,1,0
- 1,0,0,0
- 2
- 0,9.5,0,9.5
- 10,3,6,6
- 0,0,-2,0
- 12,5,10,5
- 0,0,0,4
- 0,0,20,0
- 20,0,0,0
- 2
- 1
- 4
- Normal
- SemiLight
- Normal
- Normal
- Normal
- Normal
- Normal
- Light
- Normal
- 48
- 48,0,48,27
+ 96
+ 40
+ 758
+
+
+ 0
+
+
+ 0,4,0,4
+
+
+ 12,0,12,0
diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml
index 6c8d16ddfa..e5c0babb80 100644
--- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml
@@ -2,16 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
-
-
- #FF0078D7
- #FF005A9E
- #FF004275
- #FF002642
- #FF429CE3
- #FF76B9ED
- #FFA6D8FF
-
#FFFFFFFF
#33FFFFFF
@@ -40,6 +30,9 @@
#33000000
#C50500
+ #17000000
+ #2E000000
+
@@ -135,7 +128,6 @@
-
@@ -150,196 +142,42 @@
- False
+
+
+
+
+
+
-
- XamlAutoFontFamily
- XamlAutoFontFamily
- Segoe WP
- Segoe WP SemiLight
- XamlAutoFontFamily
- Segoe MDL2 Assets
- XamlAutoFontFamily
- 24
- 48
- 56
- 24
- 40
- 3
- 374
- 0
- 2
- 0
- 21
- 64
- 80
- 240
- 14
- 14
- 320
- 548
- 184
- 756
- 130
- 202
- 32
- 32
- 56
- 80
- 20
- 20
- 758
- 456
- 40
- 96
- 240
- 4
- 34
- 20
- 14
- 80
- 16.0
- 42
- 180
- 12
- 48
- 48
- 96
- 16
- 21
- 62
- -112
- 42
- 289
- 96
- 8
- 16
- 0.6
- 4
- 2
- 0
- 14
- 32
- 32
- 270
- 300
- 28
- 4
- 26.667
- 4
- 2
- 320
- 48
- 0.8
- 0.45
- 0.5625
- 0.87
- 0.73
- 0.839
- 0.4
- 0.6
- 1
- 32
- 64
- 18.14
- 25.5
+
+ #FFFFFFFF
+
- 80
- 12
- 44
- 44
- 20
- 20
- 0
- 44
- 44
- 12
- 0,0,0,0
- 0,0,0,0
- 0,0,0,0
- 0,0,0,0
- 3,0,3,0
- 1
+ 374
0,2,0,2
+ 1
-1,0,-1,0
- 12,11,0,13
- 2
- 2
- 2
- 1
- 0
- 0,4,0,4
- 0,0,0,4
- 2
- 11,5,11,7
- 11,11,11,13
- 11,11,11,13
- 1
- 0,0,4,0
- 0,0,0,0
- 0,0,0,0
- 0,0,0,0
- 0,24,0,0
- 0,0,0,12
- 24,18,24,24
- 0,0,0,4
+ 32
+ 64
+ 456
+ 0
1
0
- 0
- 0,37,0,0
- 0,19,0,0
- 19,0,19,17.5
- 0
- 0,0,0,0
+
12,11,12,12
- 4
- 2.5
- 5,6,5,6
- 0,0,0,9
- 24,0,0,11
- 0
- 0,9.5,0,0
- 0,0,0,19
- 19,19,19,0
- 19,37,19,0
- 0,0,0,32.5
- 0
- 0
- 1
- 12,11,8,13
- 12,11,8,13
- 12,11,8,13
- 0,0,12,0
- 12,4,8,4
- 0,2,0,0
- 2
- 0
- 0,0,0,4
- 0,0,1,0
- 1,0,0,0
- 2
- 0,9.5,0,9.5
- 10,3,6,6
- 0,0,-2,0
- 12,5,10,5
- 0,0,0,4
- 0,0,20,0
- 20,0,0,0
- 2
- 1
- 4
- Normal
- SemiLight
- Normal
- Normal
- Normal
- Normal
- Normal
- Light
- Normal
- 48
- 48,0,48,27
+ 96
+ 40
+ 758
+
+
+ 0
+
+
+ 0,4,0,4
+
+
+ 12,0,12,0
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml
deleted file mode 100644
index 1c65911593..0000000000
--- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml
+++ /dev/null
@@ -1,455 +0,0 @@
-
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml
deleted file mode 100644
index ad23955776..0000000000
--- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml
+++ /dev/null
@@ -1,458 +0,0 @@
-
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
index ae1fe42031..a1f423c059 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
@@ -28,19 +28,8 @@
-
-
-
-
-
-
-
-
-
-
-
- 1
+
@@ -53,15 +42,6 @@
-
-
-
-
-
-
-
-
-
1
@@ -101,45 +81,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 21
64
- 80
- 240
1
1
- 0,0,0,4
- 2
11,5,11,7
- 11,11,11,13
- 11,11,11,13
- Normal
- SemiLight
+ Normal
-
-
+
@@ -147,17 +100,14 @@
-
-
+
-
-
+
-
-
+
@@ -182,73 +132,18 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0
0,4,0,4
-
-
-
-
-
0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ 4
+ 0
1
@@ -268,8 +163,7 @@
-
-
+
@@ -279,50 +173,43 @@
1
-
-
- 0.6
- 4
- 0
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- 12
-
+
1
- 0
@@ -362,8 +249,7 @@
-
-
+
@@ -395,23 +281,9 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
@@ -419,12 +291,9 @@
-
-
-
@@ -432,12 +301,11 @@
-
-
+
@@ -484,27 +352,11 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
4
2
0
- 0,0,0,4
Normal
@@ -527,44 +379,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
0
1
-
-
-
-
@@ -585,29 +411,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
12
@@ -615,16 +418,12 @@
-
-
-
- 8,5,8,7
+ 8,5,8,7
-
@@ -633,17 +432,14 @@
-
-
-
@@ -653,6 +449,7 @@
+
@@ -667,30 +464,22 @@
-
-
-
-
-
- XamlAutoFontFamily
24
- 40
- -25
12,0,12,0
12,0,12,0
SemiLight
-
+
@@ -705,24 +494,19 @@
-
-
+
+
0
- 1
-
-
-
-
@@ -731,46 +515,19 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 00:00:00.1
- 00:00:02
- 00:00:00.1
- 00:00:02.1
16
8
- 00:00:00.40
- 00:00:02.00
+
@@ -796,11 +553,9 @@
-
-
-
1
32
+
@@ -812,7 +567,9 @@
-
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
index 5af24f57a8..f75af76144 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
@@ -28,19 +28,8 @@
-
-
-
-
-
-
-
-
-
-
-
+
- 1
@@ -53,15 +42,7 @@
-
-
-
-
-
-
-
-
-
+
1
@@ -100,45 +81,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
- 21
- 64
- 80
- 240
+ 64
1
- 1
- 0,0,0,4
- 2
- 11,5,11,7
- 11,11,11,13
- 11,11,11,13
+ 1
+ 11,5,11,7
Normal
- SemiLight
-
@@ -147,20 +101,16 @@
-
-
-
-
@@ -182,81 +132,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0
0,4,0,4
-
-
-
-
-
-
+
0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0.6
+
+
4
0
-
-
-
-
-
+
1
32
@@ -276,7 +163,6 @@
-
@@ -286,41 +172,41 @@
1
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
- 12
-
+
1
- 0
@@ -360,8 +246,7 @@
-
-
+
@@ -393,23 +278,8 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
@@ -417,12 +287,9 @@
-
-
-
@@ -430,12 +297,11 @@
-
-
+
@@ -482,27 +348,11 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
4
2
0
- 0,0,0,4
Normal
@@ -525,44 +375,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
0
1
-
-
-
-
@@ -583,46 +407,19 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
12
1
-
-
-
8,5,8,7
-
+
-
@@ -631,17 +428,14 @@
-
-
-
@@ -651,7 +445,7 @@
-
+
@@ -665,30 +459,22 @@
-
-
-
-
-
-
+
- XamlAutoFontFamily
24
- 40
- -25
12,0,12,0
12,0,12,0
SemiLight
-
+
@@ -704,23 +490,17 @@
-
-
+
0
- 1
-
-
-
-
@@ -729,46 +509,19 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 00:00:00.1
- 00:00:02
- 00:00:00.1
- 00:00:02.1
+
16
8
- 00:00:00.40
- 00:00:02.00
+
@@ -794,11 +547,9 @@
-
-
-
1
32
+
@@ -810,7 +561,9 @@
-
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
index 2eee656dc6..60420f2f44 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
@@ -112,9 +112,7 @@
+ VerticalAlignment="Center" />
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml b/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml
index a4e716c7f6..5110d70a80 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml
@@ -1,4 +1,5 @@
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/FlyoutPresenter.xaml b/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml
index 3c2547ba85..92f8177ead 100644
--- a/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml
@@ -1,7 +1,6 @@
1
- 0
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/CompositePageTransition.cs b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs
index 9489914c97..2deebd7792 100644
--- a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs
+++ b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using Avalonia.Metadata;
@@ -35,25 +36,11 @@ namespace Avalonia.Animation
[Content]
public List PageTransitions { get; set; } = new List();
- ///
- /// Starts the animation.
- ///
- ///
- /// The control that is being transitioned away from. May be null.
- ///
- ///
- /// The control that is being transitioned to. May be null.
- ///
- ///
- /// Defines the direction of the transition.
- ///
- ///
- /// A that tracks the progress of the animation.
- ///
- public Task Start(Visual from, Visual to, bool forward)
+ ///
+ public Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken)
{
var transitionTasks = PageTransitions
- .Select(transition => transition.Start(from, to, forward))
+ .Select(transition => transition.Start(from, to, forward, cancellationToken))
.ToList();
return Task.WhenAll(transitionTasks);
}
diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs
index 0615b854da..5eaa920b32 100644
--- a/src/Avalonia.Visuals/Animation/CrossFade.cs
+++ b/src/Avalonia.Visuals/Animation/CrossFade.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.Reactive.Disposables;
+using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation.Easings;
using Avalonia.Styling;
@@ -97,49 +99,39 @@ namespace Avalonia.Animation
set => _fadeOutAnimation.Easing = value;
}
- ///
- /// Starts the animation.
- ///
- ///
- /// The control that is being transitioned away from. May be null.
- ///
- ///
- /// The control that is being transitioned to. May be null.
- ///
- ///
- /// A that tracks the progress of the animation.
- ///
- public async Task Start(Visual from, Visual to)
+ ///
+ public async Task Start(Visual from, Visual to, CancellationToken cancellationToken)
{
- var tasks = new List();
-
- if (to != null)
- {
- to.Opacity = 0;
- }
-
- if (from != null)
+ if (cancellationToken.IsCancellationRequested)
{
- tasks.Add(_fadeOutAnimation.RunAsync(from));
+ return;
}
- if (to != null)
+ var tasks = new List();
+ using (var disposables = new CompositeDisposable())
{
- to.IsVisible = true;
- tasks.Add(_fadeInAnimation.RunAsync(to));
+ if (to != null)
+ {
+ disposables.Add(to.SetValue(Visual.OpacityProperty, 0, Data.BindingPriority.Animation));
+ }
- }
+ if (from != null)
+ {
+ tasks.Add(_fadeOutAnimation.RunAsync(from, null, cancellationToken));
+ }
- await Task.WhenAll(tasks);
+ if (to != null)
+ {
+ to.IsVisible = true;
+ tasks.Add(_fadeInAnimation.RunAsync(to, null, cancellationToken));
+ }
- if (from != null)
- {
- from.IsVisible = false;
- }
+ await Task.WhenAll(tasks);
- if (to != null)
- {
- to.Opacity = 1;
+ if (from != null && !cancellationToken.IsCancellationRequested)
+ {
+ from.IsVisible = false;
+ }
}
}
@@ -155,12 +147,13 @@ namespace Avalonia.Animation
///
/// Unused for cross-fades.
///
+ /// allowed cancel transition
///
/// A that tracks the progress of the animation.
///
- Task IPageTransition.Start(Visual from, Visual to, bool forward)
+ Task IPageTransition.Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken)
{
- return Start(from, to);
+ return Start(from, to, cancellationToken);
}
}
}
diff --git a/src/Avalonia.Visuals/Animation/IPageTransition.cs b/src/Avalonia.Visuals/Animation/IPageTransition.cs
index 659bc12424..2d19ddbb5b 100644
--- a/src/Avalonia.Visuals/Animation/IPageTransition.cs
+++ b/src/Avalonia.Visuals/Animation/IPageTransition.cs
@@ -1,3 +1,4 @@
+using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Animation
@@ -19,9 +20,12 @@ namespace Avalonia.Animation
///
/// If the animation is bidirectional, controls the direction of the animation.
///
+ ///
+ /// Animation cancellation.
+ ///
///
/// A that tracks the progress of the animation.
///
- Task Start(Visual from, Visual to, bool forward);
+ Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken);
}
}
diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs
index dd5d598e12..7d033ccf61 100644
--- a/src/Avalonia.Visuals/Animation/PageSlide.cs
+++ b/src/Avalonia.Visuals/Animation/PageSlide.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation.Easings;
using Avalonia.Media;
@@ -60,23 +61,14 @@ namespace Avalonia.Animation
///
public Easing SlideOutEasing { get; set; } = new LinearEasing();
- ///
- /// Starts the animation.
- ///
- ///
- /// The control that is being transitioned away from. May be null.
- ///
- ///
- /// The control that is being transitioned to. May be null.
- ///
- ///
- /// If true, the new page is slid in from the right, or if false from the left.
- ///
- ///
- /// A that tracks the progress of the animation.
- ///
- public async Task Start(Visual from, Visual to, bool forward)
+ ///
+ public async Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken)
{
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
var tasks = new List();
var parent = GetVisualParent(from, to);
var distance = Orientation == SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height;
@@ -109,7 +101,7 @@ namespace Avalonia.Animation
},
Duration = Duration
};
- tasks.Add(animation.RunAsync(from));
+ tasks.Add(animation.RunAsync(from, null, cancellationToken));
}
if (to != null)
@@ -140,12 +132,12 @@ namespace Avalonia.Animation
},
Duration = Duration
};
- tasks.Add(animation.RunAsync(to));
+ tasks.Add(animation.RunAsync(to, null, cancellationToken));
}
await Task.WhenAll(tasks);
- if (from != null)
+ if (from != null && !cancellationToken.IsCancellationRequested)
{
from.IsVisible = false;
}
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/BrushTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs
new file mode 100644
index 0000000000..cc5af1b4b1
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs
@@ -0,0 +1,59 @@
+using System;
+using Avalonia.Animation.Animators;
+using Avalonia.Animation.Easings;
+using Avalonia.Media;
+
+#nullable enable
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Transition class that handles with type.
+ /// Only values of will transition correctly at the moment.
+ ///
+ public class BrushTransition : Transition
+ {
+ private static readonly ISolidColorBrushAnimator s_animator = new ISolidColorBrushAnimator();
+
+ public override IObservable DoTransition(IObservable progress, IBrush? oldValue, IBrush? newValue)
+ {
+ var oldSolidColorBrush = TryGetSolidColorBrush(oldValue);
+ var newSolidColorBrush = TryGetSolidColorBrush(newValue);
+
+ if (oldSolidColorBrush != null && newSolidColorBrush != null)
+ {
+ return new AnimatorTransitionObservable(
+ s_animator, progress, Easing, oldSolidColorBrush, newSolidColorBrush);
+ }
+
+ return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue);
+ }
+
+ private static ISolidColorBrush? TryGetSolidColorBrush(IBrush? brush)
+ {
+ if (brush is null)
+ {
+ return Brushes.Transparent;
+ }
+
+ return brush as ISolidColorBrush;
+ }
+
+ private class IncompatibleTransitionObservable : TransitionObservableBase
+ {
+ private readonly IBrush? _from;
+ private readonly IBrush? _to;
+
+ public IncompatibleTransitionObservable(IObservable progress, Easing easing, IBrush? from, IBrush? to) : base(progress, easing)
+ {
+ _from = from;
+ _to = to;
+ }
+
+ protected override IBrush? ProduceValue(double progress)
+ {
+ return progress < 0.5 ? _from : _to;
+ }
+ }
+ }
+}
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
index 35ba8f2b19..c917902dc3 100644
--- a/src/Avalonia.Visuals/ApiCompatBaseline.txt
+++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt
@@ -1,4 +1,10 @@
Compat issues with assembly Avalonia.Visuals:
+MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.CompositePageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.CrossFade.Start(Avalonia.Visual, Avalonia.Visual)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract.
+MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract.
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.
@@ -63,4 +69,8 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalon
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: 74
diff --git a/src/Avalonia.Visuals/Media/BoxShadow.cs b/src/Avalonia.Visuals/Media/BoxShadow.cs
index 69395fd3b8..50f75365b0 100644
--- a/src/Avalonia.Visuals/Media/BoxShadow.cs
+++ b/src/Avalonia.Visuals/Media/BoxShadow.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using System.Text;
using Avalonia.Animation.Animators;
using Avalonia.Utilities;
@@ -75,6 +76,46 @@ namespace Avalonia.Media
return rv;
}
}
+
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+
+ if (IsEmpty)
+ {
+ return "none";
+ }
+
+ if (IsInset)
+ {
+ sb.Append("inset");
+ }
+
+ if (OffsetX != 0.0)
+ {
+ sb.AppendFormat(" {0}", OffsetX.ToString());
+ }
+
+ if (OffsetY != 0.0)
+ {
+ sb.AppendFormat(" {0}", OffsetY.ToString());
+ }
+
+ if (Blur != 0.0)
+ {
+ sb.AppendFormat(" {0}", Blur.ToString());
+ }
+
+ if (Spread != 0.0)
+ {
+ sb.AppendFormat(" {0}", Spread.ToString());
+ }
+
+ sb.AppendFormat(" {0}", Color.ToString());
+
+ return sb.ToString();
+ }
+
public static unsafe BoxShadow Parse(string s)
{
if(s == null)
diff --git a/src/Avalonia.Visuals/Media/BoxShadows.cs b/src/Avalonia.Visuals/Media/BoxShadows.cs
index 9e4d6aacb0..810ac70b99 100644
--- a/src/Avalonia.Visuals/Media/BoxShadows.cs
+++ b/src/Avalonia.Visuals/Media/BoxShadows.cs
@@ -1,6 +1,6 @@
using System;
-using System.Collections.Generic;
using System.ComponentModel;
+using System.Text;
using Avalonia.Animation.Animators;
namespace Avalonia.Media
@@ -43,6 +43,24 @@ namespace Avalonia.Media
}
}
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+
+ if (Count == 0)
+ {
+ return "none";
+ }
+
+ foreach (var boxShadow in this)
+ {
+ sb.AppendFormat("{0} ", boxShadow.ToString());
+ }
+
+ return sb.ToString();
+
+ }
+
[EditorBrowsable(EditorBrowsableState.Never)]
public struct BoxShadowsEnumerator
{
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/ImageDrawing.cs b/src/Avalonia.Visuals/Media/ImageDrawing.cs
new file mode 100644
index 0000000000..82f97b52b4
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/ImageDrawing.cs
@@ -0,0 +1,53 @@
+#nullable enable
+
+namespace Avalonia.Media
+{
+ ///
+ /// Draws an image within a region defined by a .
+ ///
+ public class ImageDrawing : Drawing
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ImageSourceProperty =
+ AvaloniaProperty.Register(nameof(ImageSource));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty RectProperty =
+ AvaloniaProperty.Register(nameof(Rect));
+
+ ///
+ /// Gets or sets the source of the image.
+ ///
+ public IImage? ImageSource
+ {
+ get => GetValue(ImageSourceProperty);
+ set => SetValue(ImageSourceProperty, value);
+ }
+
+ ///
+ /// Gets or sets region in which the image is drawn.
+ ///
+ public Rect Rect
+ {
+ get => GetValue(RectProperty);
+ set => SetValue(RectProperty, value);
+ }
+
+ public override void Draw(DrawingContext context)
+ {
+ var imageSource = ImageSource;
+ var rect = Rect;
+
+ if (imageSource is object && !rect.IsEmpty)
+ {
+ context.DrawImage(imageSource, rect);
+ }
+ }
+
+ public override Rect GetBounds() => Rect;
+ }
+}
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/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs
index e9a52fe6ed..2dd188e0a9 100644
--- a/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs
+++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs
@@ -10,6 +10,8 @@ namespace Avalonia.Media.Immutable
///
public class ImmutableDashStyle : IDashStyle, IEquatable
{
+ private readonly double[] _dashes;
+
///
/// Initializes a new instance of the class.
///
@@ -17,12 +19,12 @@ namespace Avalonia.Media.Immutable
/// The dash sequence offset.
public ImmutableDashStyle(IEnumerable dashes, double offset)
{
- Dashes = (IReadOnlyList)dashes?.ToList() ?? Array.Empty();
+ _dashes = dashes?.ToArray() ?? Array.Empty();
Offset = offset;
}
///
- public IReadOnlyList Dashes { get; }
+ public IReadOnlyList Dashes => _dashes;
///
public double Offset { get; }
@@ -56,9 +58,9 @@ namespace Avalonia.Media.Immutable
var hashCode = 717868523;
hashCode = hashCode * -1521134295 + Offset.GetHashCode();
- if (Dashes != null)
+ if (_dashes != null)
{
- foreach (var i in Dashes)
+ foreach (var i in _dashes)
{
hashCode = hashCode * -1521134295 + i.GetHashCode();
}
@@ -69,7 +71,7 @@ namespace Avalonia.Media.Immutable
private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList right)
{
- if (left == right)
+ if (ReferenceEquals(left, right))
{
return true;
}
diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs
index e586eaf3a9..3256f4b11a 100644
--- a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs
+++ b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
namespace Avalonia.Media.Immutable
{
@@ -23,7 +24,7 @@ namespace Avalonia.Media.Immutable
ImmutableDashStyle dashStyle = null,
PenLineCap lineCap = PenLineCap.Flat,
PenLineJoin lineJoin = PenLineJoin.Miter,
- double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit)
+ double miterLimit = 10.0) : this(new ImmutableSolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit)
{
}
@@ -44,6 +45,8 @@ namespace Avalonia.Media.Immutable
PenLineJoin lineJoin = PenLineJoin.Miter,
double miterLimit = 10.0)
{
+ Debug.Assert(!(brush is IMutableBrush));
+
Brush = brush;
Thickness = thickness;
LineCap = lineCap;
diff --git a/src/Avalonia.Visuals/Media/PolyLineSegment.cs b/src/Avalonia.Visuals/Media/PolyLineSegment.cs
new file mode 100644
index 0000000000..55bfb33041
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/PolyLineSegment.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using Avalonia.Collections;
+
+namespace Avalonia.Media
+{
+ ///
+ /// Represents a set of line segments defined by a points collection with each Point specifying the end point of a line segment.
+ ///
+ public sealed class PolyLineSegment : PathSegment
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PointsProperty
+ = AvaloniaProperty.Register(nameof(Points));
+
+ ///
+ /// Gets or sets the points.
+ ///
+ ///
+ /// The points.
+ ///
+ public AvaloniaList Points
+ {
+ get => GetValue(PointsProperty);
+ set => SetValue(PointsProperty, value);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PolyLineSegment()
+ {
+ Points = new Points();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The points.
+ public PolyLineSegment(IEnumerable points) : this()
+ {
+ Points.AddRange(points);
+ }
+
+ protected internal override void ApplyTo(StreamGeometryContext ctx)
+ {
+ var points = Points;
+ if (points.Count > 0)
+ {
+ for (int i = 0; i < points.Count; i++)
+ {
+ ctx.LineTo(points[i]);
+ }
+ }
+ }
+
+ public override string ToString()
+ => Points.Count >= 1 ? "L " + string.Join(" ", Points) : "";
+ }
+}
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/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
index 723d5e81ab..b304b19910 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
@@ -109,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);
@@ -120,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 c6f524451b..0779716ec8 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
@@ -135,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)
@@ -161,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/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
index 300d61f81d..6533c34ba0 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
@@ -422,7 +422,7 @@ namespace Avalonia.Media.TextFormatting
}
else
{
- currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+ currentPosition = currentLength + measuredLength;
}
breakFound = true;
diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
index f0dbfee718..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.
///
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
index 0a5c1f8db6..45b62b843b 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs
@@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph
}
///
- /// Initializes a new instance of the class that represents an
+ /// Initializes a new instance of the class that represents an
/// pop.
///
public BitmapBlendModeNode()
@@ -40,7 +40,7 @@ namespace Avalonia.Rendering.SceneGraph
///
/// Determines if this draw operation equals another.
///
- /// The opacity of the other draw operation.
+ /// the how to compare
/// True if the draw operations are the same, otherwise false.
///
/// The properties of the other draw operation are passed in as arguments to prevent
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
index e6092574c5..3594cb59ee 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
@@ -408,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/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..d8e5baac97 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 ICustomHitTest 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..f2a09b815e 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,28 @@ namespace Avalonia.Rendering.SceneGraph
if (forceRecurse)
{
- foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance))
+ var visualChildren = (IList) visual.VisualChildren;
+
+ node.TryPreallocateChildren(visualChildren.Count);
+
+ 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 +325,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 +344,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..db6b606b41 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
@@ -358,6 +358,11 @@ namespace Avalonia.Rendering.SceneGraph
internal void TryPreallocateChildren(int count)
{
+ if (count == 0)
+ {
+ return;
+ }
+
EnsureChildrenCreated(count);
}
@@ -365,9 +370,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/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/TransparencyHelper.cs b/src/Avalonia.X11/TransparencyHelper.cs
index 0578680136..2140b61b6f 100644
--- a/src/Avalonia.X11/TransparencyHelper.cs
+++ b/src/Avalonia.X11/TransparencyHelper.cs
@@ -10,7 +10,6 @@ namespace Avalonia.X11
private readonly X11Globals _globals;
private WindowTransparencyLevel _currentLevel;
private WindowTransparencyLevel _requestedLevel;
- private bool _isCompositing;
private bool _blurAtomsAreSet;
public Action TransparencyLevelChanged { get; set; }
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 5ac4c4c9d0..37260aa78b 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -30,7 +30,6 @@ namespace Avalonia.X11
ITopLevelImplWithTextInputMethod
{
private readonly AvaloniaX11Platform _platform;
- private readonly IWindowImpl _popupParent;
private readonly bool _popup;
private readonly X11Info _x11;
private XConfigureEvent? _configure;
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
index 4bbb58e53e..ac2fd40c54 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
@@ -15,7 +15,6 @@ namespace Avalonia.LinuxFramebuffer
private readonly IOutputBackend _outputBackend;
private readonly IInputBackend _inputBackend;
- private bool _renderQueued;
public IInputRoot InputRoot { get; private set; }
public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend)
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..c35a3d1174
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs
@@ -0,0 +1,113 @@
+using System;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+
+namespace Avalonia.LinuxFramebuffer.Input.EvDev
+{
+ internal class EvDevSingleTouchScreen : EvDevDeviceHandler
+ {
+ private readonly IScreenInfoProvider _screenInfo;
+ 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..89f81a7649 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();
@@ -114,7 +132,10 @@ public static class LinuxFramebufferPlatformExtensions
{
public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1)
where T : AppBuilderBase, new() =>
- StartLinuxDirect(builder, args, new FbdevOutput(fbdev) {Scaling = scaling});
+ StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling });
+ public static int StartLinuxFbDev(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling)
+ where T : AppBuilderBase, new() =>
+ StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling });
public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1)
where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) {Scaling = scaling});
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs
index ed59166eb8..87c7b64c26 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs
@@ -41,6 +41,6 @@ namespace Avalonia.LinuxFramebuffer
public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres);
public int RowBytes => (int) _fixedInfo.line_length;
public Vector Dpi { get; }
- public PixelFormat Format => _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888;
+ public PixelFormat Format => _varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 : _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888;
}
}
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/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
index dc44d2d55f..ee4125101c 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
@@ -16,7 +16,6 @@ namespace Avalonia.LinuxFramebuffer.Output
public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface
{
private DrmCard _card;
- private readonly EglGlPlatformSurface _eglPlatformSurface;
public PixelSize PixelSize => _mode.Resolution;
public double Scaling { get; set; }
public IGlContext PrimaryContext => _deferredContext;
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
index b83fe6cbe8..61f00b2795 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
@@ -16,16 +16,33 @@ namespace Avalonia.LinuxFramebuffer
private IntPtr _mappedAddress;
public double Scaling { get; set; }
- public FbdevOutput(string fileName = null)
+ ///
+ /// Create a Linux frame buffer device output
+ ///
+ /// The frame buffer device name.
+ /// Defaults to the value in environment variable FRAMEBUFFER or /dev/fb0 when FRAMEBUFFER is not set
+ public FbdevOutput(string fileName = null) : this(fileName, null)
{
- fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0";
+ }
+
+ ///
+ /// Create a Linux frame buffer device output
+ ///
+ /// The frame buffer device name.
+ /// Defaults to the value in environment variable FRAMEBUFFER or /dev/fb0 when FRAMEBUFFER is not set
+ /// The required pixel format for the frame buffer.
+ /// A null value will leave the frame buffer in the current pixel format.
+ /// Otherwise sets the frame buffer to the required format
+ public FbdevOutput(string fileName, PixelFormat? format)
+ {
+ fileName ??= Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0";
_fd = NativeUnsafeMethods.open(fileName, 2, 0);
if (_fd <= 0)
throw new Exception("Error: " + Marshal.GetLastWin32Error());
try
{
- Init();
+ Init(format);
}
catch
{
@@ -34,25 +51,28 @@ namespace Avalonia.LinuxFramebuffer
}
}
- void Init()
+ void Init(PixelFormat? format)
{
fixed (void* pnfo = &_varInfo)
{
if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo))
throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error());
- SetBpp();
+ if (format.HasValue)
+ {
+ SetBpp(format.Value);
- if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo))
- _varInfo.transp = new fb_bitfield();
+ if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo))
+ _varInfo.transp = new fb_bitfield();
- NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo);
+ NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo);
- if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo))
- throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error());
+ if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo))
+ throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error());
- if (_varInfo.bits_per_pixel != 32)
- throw new Exception("Unable to set 32-bit display mode");
+ if (_varInfo.bits_per_pixel != 32)
+ throw new Exception("Unable to set 32-bit display mode");
+ }
}
fixed(void*pnfo = &_fixedInfo)
if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_FSCREENINFO, pnfo))
@@ -70,17 +90,43 @@ namespace Avalonia.LinuxFramebuffer
}
}
- void SetBpp()
+ void SetBpp(PixelFormat format)
{
- _varInfo.bits_per_pixel = 32;
- _varInfo.grayscale = 0;
- _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield
+ switch (format)
{
- length = 8
- };
- _varInfo.green.offset = 8;
- _varInfo.blue.offset = 16;
- _varInfo.transp.offset = 24;
+ case PixelFormat.Rgba8888:
+ _varInfo.bits_per_pixel = 32;
+ _varInfo.grayscale = 0;
+ _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield
+ {
+ length = 8
+ };
+ _varInfo.green.offset = 8;
+ _varInfo.blue.offset = 16;
+ _varInfo.transp.offset = 24;
+ break;
+ case PixelFormat.Bgra8888:
+ _varInfo.bits_per_pixel = 32;
+ _varInfo.grayscale = 0;
+ _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield
+ {
+ length = 8
+ };
+ _varInfo.green.offset = 8;
+ _varInfo.red.offset = 16;
+ _varInfo.transp.offset = 24;
+ break;
+ case PixelFormat.Rgb565:
+ _varInfo.bits_per_pixel = 16;
+ _varInfo.grayscale = 0;
+ _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield();
+ _varInfo.red.length = 5;
+ _varInfo.green.offset = 5;
+ _varInfo.green.length = 6;
+ _varInfo.blue.offset = 11;
+ _varInfo.blue.length = 5;
+ break;
+ }
}
public string Id { get; private set; }
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index b7d5d3ec59..2352b8b076 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -164,9 +164,12 @@ namespace Avalonia.Skia
///
public void DrawLine(IPen pen, Point p1, Point p2)
{
- using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
+ using (var paint = CreatePaint(_strokePaint, pen, new Rect(p1, p2).Normalize()))
{
- 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);
+ }
}
}
@@ -174,10 +177,10 @@ namespace Avalonia.Skia
public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
{
var impl = (GeometryImpl) geometry;
- var size = geometry.Bounds.Size;
+ var rect = geometry.Bounds;
- using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper))
- using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper))
+ using (var fill = brush != null ? CreatePaint(_fillPaint, brush, rect) : default(PaintWrapper))
+ using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, rect) : default(PaintWrapper))
{
if (fill.Paint != null)
{
@@ -351,7 +354,7 @@ namespace Avalonia.Skia
if (brush != null)
{
- using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size))
+ using (var paint = CreatePaint(_fillPaint, brush, rect.Rect))
{
if (isRounded)
{
@@ -361,7 +364,6 @@ namespace Avalonia.Skia
{
Canvas.DrawRect(rc, paint.Paint);
}
-
}
}
@@ -395,17 +397,19 @@ namespace Avalonia.Skia
if (pen?.Brush != null)
{
- using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size))
+ using (var paint = CreatePaint(_strokePaint, pen, rect.Rect))
{
- 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);
+ }
}
-
}
}
}
@@ -413,7 +417,7 @@ namespace Avalonia.Skia
///
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{
- using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size))
+ using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds))
{
var textImpl = (FormattedTextImpl) text;
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
@@ -423,7 +427,7 @@ namespace Avalonia.Skia
///
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
- using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size))
+ using (var paintWrapper = CreatePaint(_fillPaint, foreground, new Rect(glyphRun.Size)))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
@@ -533,7 +537,7 @@ namespace Avalonia.Skia
var paint = new SKPaint();
Canvas.SaveLayer(paint);
- _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true));
+ _maskStack.Push(CreatePaint(paint, mask, bounds, true));
}
///
@@ -587,20 +591,21 @@ namespace Avalonia.Skia
/// Configure paint wrapper for using gradient brush.
///
/// Paint wrapper.
- /// Target size.
+ /// Target bound rect.
/// Gradient brush.
- private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush)
+ private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Rect targetRect, IGradientBrush gradientBrush)
{
var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode();
var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray();
+ var position = targetRect.Position.ToSKPoint();
switch (gradientBrush)
{
case ILinearGradientBrush linearGradient:
{
- var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint();
- var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint();
+ var start = position + linearGradient.StartPoint.ToPixels(targetRect.Size).ToSKPoint();
+ var end = position + linearGradient.EndPoint.ToPixels(targetRect.Size).ToSKPoint();
// would be nice to cache these shaders possibly?
using (var shader =
@@ -613,10 +618,10 @@ namespace Avalonia.Skia
}
case IRadialGradientBrush radialGradient:
{
- var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
- var radius = (float)(radialGradient.Radius * targetSize.Width);
+ var center = position + radialGradient.Center.ToPixels(targetRect.Size).ToSKPoint();
+ var radius = (float)(radialGradient.Radius * targetRect.Width);
- var origin = radialGradient.GradientOrigin.ToPixels(targetSize).ToSKPoint();
+ var origin = position + radialGradient.GradientOrigin.ToPixels(targetRect.Size).ToSKPoint();
if (origin.Equals(center))
{
@@ -661,7 +666,7 @@ namespace Avalonia.Skia
}
case IConicGradientBrush conicGradient:
{
- var center = conicGradient.Center.ToPixels(targetSize).ToSKPoint();
+ var center = position + conicGradient.Center.ToPixels(targetRect.Size).ToSKPoint();
// Skia's default is that angle 0 is from the right hand side of the center point
// but we are matching CSS where the vertical point above the center is 0.
@@ -863,10 +868,10 @@ namespace Avalonia.Skia
///
/// The paint to wrap.
/// Source brush.
- /// Target size.
+ /// Target rect.
/// Optional dispose of the supplied paint.
/// Paint wrapper for given brush.
- internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false)
+ internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Rect targetRect, bool disposePaint = false)
{
var paintWrapper = new PaintWrapper(paint, disposePaint);
@@ -885,7 +890,7 @@ namespace Avalonia.Skia
if (brush is IGradientBrush gradient)
{
- ConfigureGradientBrush(ref paintWrapper, targetSize, gradient);
+ ConfigureGradientBrush(ref paintWrapper, targetRect, gradient);
return paintWrapper;
}
@@ -905,7 +910,7 @@ namespace Avalonia.Skia
if (tileBrush != null && tileBrushImage != null)
{
- ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage);
+ ConfigureTileBrush(ref paintWrapper, targetRect.Size, tileBrush, tileBrushImage);
}
else
{
@@ -920,10 +925,10 @@ namespace Avalonia.Skia
///
/// The paint to wrap.
/// Source pen.
- /// Target size.
+ /// Target rect.
/// Optional dispose of the supplied paint.
///
- private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false)
+ private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Rect targetRect, bool disposePaint = false)
{
// In Skia 0 thickness means - use hairline rendering
// and for us it means - there is nothing rendered.
@@ -932,7 +937,7 @@ namespace Avalonia.Skia
return default;
}
- var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint);
+ var rv = CreatePaint(paint, pen.Brush, targetRect, disposePaint);
paint.IsStroke = true;
paint.StrokeWidth = (float) pen.Thickness;
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
index 5e630e54a6..3eca42faa9 100644
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using Avalonia.Media;
using Avalonia.Platform;
@@ -175,7 +176,8 @@ namespace Avalonia.Skia
foreach (var line in _skiaLines.Where(l =>
(l.Start + l.Length) > index &&
- lastIndex >= l.Start))
+ lastIndex >= l.Start &&
+ !l.IsEmptyTrailingLine))
{
int lineEndIndex = line.Start + (line.Length > 0 ? line.Length - 1 : 0);
@@ -276,9 +278,9 @@ namespace Avalonia.Skia
if (fb != null)
{
- //TODO: figure out how to get the brush size
+ //TODO: figure out how to get the brush rect
currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb,
- new Size());
+ default);
}
else
{
@@ -466,7 +468,8 @@ namespace Avalonia.Skia
for (int i = line.Start; i < line.Start + line.TextLength; i++)
{
- float w = _paint.MeasureText(Text[i].ToString());
+ var c = Text[i];
+ var w = line.IsEmptyTrailingLine ? 0 :_paint.MeasureText(Text[i].ToString());
_rects.Add(new Rect(
prevRight,
@@ -611,6 +614,7 @@ namespace Avalonia.Skia
lastLine.Width = lastLineWidth;
lastLine.Height = _lineHeight;
lastLine.Top = curY;
+ lastLine.IsEmptyTrailingLine = true;
_skiaLines.Add(lastLine);
@@ -713,6 +717,7 @@ namespace Avalonia.Skia
public int TextLength;
public float Top;
public float Width;
+ public bool IsEmptyTrailingLine;
};
private struct FBrushRange
diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs
index aa86df7c23..32818dfdd2 100644
--- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs
+++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs
@@ -16,11 +16,12 @@ namespace Avalonia.Skia
/// Surfaces.
/// Created render target or if it fails.
ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces);
-
+
///
/// Creates an offscreen render target surface
///
/// size in pixels
+ /// current Skia render session
ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session);
}
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index 6b7f0e11cf..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)
{
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 15685c0187..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)
{
diff --git a/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs b/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs
index 22c998df93..e4b2405290 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs
@@ -34,10 +34,10 @@ namespace Avalonia.Direct2D1.Media
{
var wrapper = clientDrawingEffect as BrushWrapper;
- // TODO: Work out how to get the size below rather than passing new Size().
+ // TODO: Work out how to get the rect below rather than passing default.
var brush = (wrapper == null) ?
_foreground :
- _context.CreateBrush(wrapper.Brush, new Size()).PlatformBrush;
+ _context.CreateBrush(wrapper.Brush, default).PlatformBrush;
_renderTarget.DrawGlyphRun(
new RawVector2 { X = baselineOriginX, Y = baselineOriginY },
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
index af35934785..9336c9a7bb 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
@@ -192,7 +192,7 @@ namespace Avalonia.Direct2D1.Media
{
using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext))
using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value))
- using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size))
+ using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect))
using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(Direct2D1Platform.Direct2D1Factory, destRect.ToDirect2D()))
{
if (d2dOpacityMask.PlatformBrush != null)
@@ -217,9 +217,7 @@ namespace Avalonia.Direct2D1.Media
{
if (pen != null)
{
- var size = new Rect(p1, p2).Size;
-
- using (var d2dBrush = CreateBrush(pen.Brush, size))
+ using (var d2dBrush = CreateBrush(pen.Brush, new Rect(p1, p2).Normalize()))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (d2dBrush.PlatformBrush != null)
@@ -245,7 +243,7 @@ namespace Avalonia.Direct2D1.Media
{
if (brush != null)
{
- using (var d2dBrush = CreateBrush(brush, geometry.Bounds.Size))
+ using (var d2dBrush = CreateBrush(brush, geometry.Bounds))
{
if (d2dBrush.PlatformBrush != null)
{
@@ -257,7 +255,7 @@ namespace Avalonia.Direct2D1.Media
if (pen != null)
{
- using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen).Size))
+ using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen)))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (d2dBrush.PlatformBrush != null)
@@ -282,7 +280,7 @@ namespace Avalonia.Direct2D1.Media
if (brush != null)
{
- using (var b = CreateBrush(brush, rect.Size))
+ using (var b = CreateBrush(brush, rect))
{
if (b.PlatformBrush != null)
{
@@ -311,7 +309,7 @@ namespace Avalonia.Direct2D1.Media
if (pen?.Brush != null)
{
- using (var wrapper = CreateBrush(pen.Brush, rect.Size))
+ using (var wrapper = CreateBrush(pen.Brush, rect))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (wrapper.PlatformBrush != null)
@@ -349,7 +347,7 @@ namespace Avalonia.Direct2D1.Media
{
var impl = (FormattedTextImpl)text;
- using (var brush = CreateBrush(foreground, impl.Bounds.Size))
+ using (var brush = CreateBrush(foreground, impl.Bounds))
using (var renderer = new AvaloniaTextRenderer(this, _deviceContext, brush.PlatformBrush))
{
if (brush.PlatformBrush != null)
@@ -367,7 +365,7 @@ namespace Avalonia.Direct2D1.Media
/// The glyph run.
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
- using (var brush = CreateBrush(foreground, glyphRun.Size))
+ using (var brush = CreateBrush(foreground, new Rect(glyphRun.Size)))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
@@ -458,9 +456,9 @@ namespace Avalonia.Direct2D1.Media
/// Creates a Direct2D brush wrapper for a Avalonia brush.
///
/// The avalonia brush.
- /// The size of the brush's target area.
+ /// The brush's target area.
/// The Direct2D brush wrapper.
- public BrushImpl CreateBrush(IBrush brush, Size destinationSize)
+ public BrushImpl CreateBrush(IBrush brush, Rect destinationRect)
{
var solidColorBrush = brush as ISolidColorBrush;
var linearGradientBrush = brush as ILinearGradientBrush;
@@ -475,11 +473,11 @@ namespace Avalonia.Direct2D1.Media
}
else if (linearGradientBrush != null)
{
- return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationSize);
+ return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationRect);
}
else if (radialGradientBrush != null)
{
- return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationSize);
+ return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationRect);
}
else if (conicGradientBrush != null)
{
@@ -492,7 +490,7 @@ namespace Avalonia.Direct2D1.Media
imageBrush,
_deviceContext,
(BitmapImpl)imageBrush.Source.PlatformImpl.Item,
- destinationSize);
+ destinationRect.Size);
}
else if (visualBrush != null)
{
@@ -523,7 +521,7 @@ namespace Avalonia.Direct2D1.Media
visualBrush,
_deviceContext,
new D2DBitmapImpl(intermediate.Bitmap),
- destinationSize);
+ destinationRect.Size);
}
}
}
@@ -574,7 +572,7 @@ namespace Avalonia.Direct2D1.Media
ContentBounds = PrimitiveExtensions.RectangleInfinite,
MaskTransform = PrimitiveExtensions.Matrix3x2Identity,
Opacity = 1,
- OpacityBrush = CreateBrush(mask, bounds.Size).PlatformBrush
+ OpacityBrush = CreateBrush(mask, bounds).PlatformBrush
};
var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_deviceContext);
_deviceContext.PushLayer(ref parameters, layer);
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.Direct2D1/Media/LinearGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs
index 0e63d4cc03..69b45455ac 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs
@@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media
public LinearGradientBrushImpl(
ILinearGradientBrush brush,
SharpDX.Direct2D1.RenderTarget target,
- Size destinationSize)
+ Rect destinationRect)
{
if (brush.GradientStops.Count == 0)
{
@@ -21,8 +21,9 @@ namespace Avalonia.Direct2D1.Media
Position = (float)s.Offset
}).ToArray();
- var startPoint = brush.StartPoint.ToPixels(destinationSize);
- var endPoint = brush.EndPoint.ToPixels(destinationSize);
+ var position = destinationRect.Position;
+ var startPoint = position + brush.StartPoint.ToPixels(destinationRect.Size);
+ var endPoint = position + brush.EndPoint.ToPixels(destinationRect.Size);
using (var stops = new SharpDX.Direct2D1.GradientStopCollection(
target,
diff --git a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs
index 1fca6d4e33..7dcfd7e1e0 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs
@@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media
public RadialGradientBrushImpl(
IRadialGradientBrush brush,
SharpDX.Direct2D1.RenderTarget target,
- Size destinationSize)
+ Rect destinationRect)
{
if (brush.GradientStops.Count == 0)
{
@@ -21,12 +21,13 @@ namespace Avalonia.Direct2D1.Media
Position = (float)s.Offset
}).ToArray();
- var centerPoint = brush.Center.ToPixels(destinationSize);
- var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize) - centerPoint;
+ var position = destinationRect.Position;
+ var centerPoint = position + brush.Center.ToPixels(destinationRect.Size);
+ var gradientOrigin = position + brush.GradientOrigin.ToPixels(destinationRect.Size) - centerPoint;
// Note: Direct2D supports RadiusX and RadiusY but Cairo backend supports only Radius property
- var radiusX = brush.Radius * destinationSize.Width;
- var radiusY = brush.Radius * destinationSize.Height;
+ var radiusX = brush.Radius * destinationRect.Width;
+ var radiusY = brush.Radius * destinationRect.Height;
using (var stops = new SharpDX.Direct2D1.GradientStopCollection(
target,
diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
index 2aa82436f6..1c3c959acf 100644
--- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
+++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
@@ -17,7 +17,6 @@ namespace Avalonia.Win32.WinRT.Composition
class WinUICompositorConnection : IRenderTimer
{
private readonly EglContext _syncContext;
- private IntPtr _queue;
private ICompositor _compositor;
private ICompositor2 _compositor2;
private ICompositor5 _compositor5;
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs
index d886b67241..8a340aac5e 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs
@@ -117,7 +117,7 @@ namespace Avalonia.Win32
{
var visual = window.Renderer.HitTestFirst(position, _owner as Window, x =>
{
- if (x is IInputElement ie && !ie.IsHitTestVisible)
+ if (x is IInputElement ie && (!ie.IsHitTestVisible || !ie.IsVisible))
{
return false;
}
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 082aca1109..3a3342fd14 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -900,7 +900,7 @@ namespace Avalonia.Win32
IntPtr.Zero,
rcWindow.left, rcWindow.top,
rcClient.Width, rcClient.Height,
- SetWindowPosFlags.SWP_FRAMECHANGED);
+ SetWindowPosFlags.SWP_FRAMECHANGED | SetWindowPosFlags.SWP_NOACTIVATE);
if (_isClientAreaExtended && WindowState != WindowState.FullScreen)
{
diff --git a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs
index fe718ec32b..58bd7a42c3 100644
--- a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs
+++ b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs
@@ -9,6 +9,8 @@ using Avalonia.UnitTests;
using Avalonia.Data;
using Xunit;
using Avalonia.Animation.Easings;
+using System.Threading;
+using System.Reactive.Linq;
namespace Avalonia.Animation.UnitTests
{
@@ -176,5 +178,271 @@ namespace Avalonia.Animation.UnitTests
clock.Step(TimeSpan.FromSeconds(0.100d));
Assert.Equal(border.Width, 300d);
}
+
+ [Fact(Skip = "See #6111")]
+ public void Dispose_Subscription_Should_Stop_Animation()
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters =
+ {
+ new Setter(Border.WidthProperty, 200d),
+ },
+ Cue = new Cue(1d)
+ };
+
+ var keyframe2 = new KeyFrame()
+ {
+ Setters =
+ {
+ new Setter(Border.WidthProperty, 100d),
+ },
+ Cue = new Cue(0d)
+ };
+
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(10),
+ Delay = TimeSpan.FromSeconds(0),
+ DelayBetweenIterations = TimeSpan.FromSeconds(0),
+ IterationCount = new IterationCount(1),
+ Children =
+ {
+ keyframe2,
+ keyframe1
+ }
+ };
+
+ var border = new Border()
+ {
+ Height = 100d,
+ Width = 50d
+ };
+ var propertyChangedCount = 0;
+ var animationCompletedCount = 0;
+ border.PropertyChanged += (sender, e) =>
+ {
+ if (e.Property == Control.WidthProperty)
+ {
+ propertyChangedCount++;
+ }
+ };
+
+ var clock = new TestClock();
+ var disposable = animation.Apply(border, clock, Observable.Return(true), () => animationCompletedCount++);
+
+ Assert.Equal(0, propertyChangedCount);
+
+ clock.Step(TimeSpan.FromSeconds(0));
+ Assert.Equal(0, animationCompletedCount);
+ Assert.Equal(1, propertyChangedCount);
+
+ disposable.Dispose();
+
+ // Clock ticks should be ignored after Dispose
+ clock.Step(TimeSpan.FromSeconds(5));
+ clock.Step(TimeSpan.FromSeconds(6));
+ clock.Step(TimeSpan.FromSeconds(7));
+
+ // On animation disposing (cancellation) on completed is not invoked (is it expected?)
+ Assert.Equal(0, animationCompletedCount);
+ // Initial property changed before cancellation + animation value removal.
+ Assert.Equal(2, propertyChangedCount);
+ }
+
+ [Fact]
+ public void Do_Not_Run_Cancelled_Animation()
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters =
+ {
+ new Setter(Border.WidthProperty, 200d),
+ },
+ Cue = new Cue(1d)
+ };
+
+ var keyframe2 = new KeyFrame()
+ {
+ Setters =
+ {
+ new Setter(Border.WidthProperty, 100d),
+ },
+ Cue = new Cue(0d)
+ };
+
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(10),
+ Delay = TimeSpan.FromSeconds(0),
+ DelayBetweenIterations = TimeSpan.FromSeconds(0),
+ IterationCount = new IterationCount(1),
+ Children =
+ {
+ keyframe2,
+ keyframe1
+ }
+ };
+
+ var border = new Border()
+ {
+ Height = 100d,
+ Width = 100d
+ };
+ var propertyChangedCount = 0;
+ border.PropertyChanged += (sender, e) =>
+ {
+ if (e.Property == Control.WidthProperty)
+ {
+ propertyChangedCount++;
+ }
+ };
+
+ var clock = new TestClock();
+ var cancellationTokenSource = new CancellationTokenSource();
+ cancellationTokenSource.Cancel();
+ var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token);
+
+ clock.Step(TimeSpan.FromSeconds(10));
+ Assert.Equal(0, propertyChangedCount);
+ Assert.True(animationRun.IsCompleted);
+ }
+
+ [Fact(Skip = "See #6111")]
+ public void Cancellation_Should_Stop_Animation()
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters =
+ {
+ new Setter(Border.WidthProperty, 200d),
+ },
+ Cue = new Cue(1d)
+ };
+
+ var keyframe2 = new KeyFrame()
+ {
+ Setters =
+ {
+ new Setter(Border.WidthProperty, 100d),
+ },
+ Cue = new Cue(0d)
+ };
+
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(10),
+ Delay = TimeSpan.FromSeconds(0),
+ DelayBetweenIterations = TimeSpan.FromSeconds(0),
+ IterationCount = new IterationCount(1),
+ Children =
+ {
+ keyframe2,
+ keyframe1
+ }
+ };
+
+ var border = new Border()
+ {
+ Height = 100d,
+ Width = 50d
+ };
+ var propertyChangedCount = 0;
+ border.PropertyChanged += (sender, e) =>
+ {
+ if (e.Property == Control.WidthProperty)
+ {
+ propertyChangedCount++;
+ }
+ };
+
+ var clock = new TestClock();
+ var cancellationTokenSource = new CancellationTokenSource();
+ var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token);
+
+ Assert.Equal(0, propertyChangedCount);
+
+ clock.Step(TimeSpan.FromSeconds(0));
+ Assert.False(animationRun.IsCompleted);
+ Assert.Equal(1, propertyChangedCount);
+
+ cancellationTokenSource.Cancel();
+ clock.Step(TimeSpan.FromSeconds(1));
+ clock.Step(TimeSpan.FromSeconds(2));
+ clock.Step(TimeSpan.FromSeconds(3));
+ //Assert.Equal(2, propertyChangedCount);
+
+ animationRun.Wait();
+
+ clock.Step(TimeSpan.FromSeconds(6));
+ Assert.True(animationRun.IsCompleted);
+ Assert.Equal(2, propertyChangedCount);
+ }
+
+ [Fact]
+ public void Cancellation_Of_Completed_Animation_Does_Not_Fail()
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters =
+ {
+ new Setter(Border.WidthProperty, 200d),
+ },
+ Cue = new Cue(1d)
+ };
+
+ var keyframe2 = new KeyFrame()
+ {
+ Setters =
+ {
+ new Setter(Border.WidthProperty, 100d),
+ },
+ Cue = new Cue(0d)
+ };
+
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(10),
+ Delay = TimeSpan.FromSeconds(0),
+ DelayBetweenIterations = TimeSpan.FromSeconds(0),
+ IterationCount = new IterationCount(1),
+ Children =
+ {
+ keyframe2,
+ keyframe1
+ }
+ };
+
+ var border = new Border()
+ {
+ Height = 100d,
+ Width = 50d
+ };
+ var propertyChangedCount = 0;
+ border.PropertyChanged += (sender, e) =>
+ {
+ if (e.Property == Control.WidthProperty)
+ {
+ propertyChangedCount++;
+ }
+ };
+
+ var clock = new TestClock();
+ var cancellationTokenSource = new CancellationTokenSource();
+ var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token);
+
+ Assert.Equal(0, propertyChangedCount);
+
+ clock.Step(TimeSpan.FromSeconds(0));
+ Assert.False(animationRun.IsCompleted);
+ Assert.Equal(1, propertyChangedCount);
+
+ clock.Step(TimeSpan.FromSeconds(10));
+ Assert.True(animationRun.IsCompleted);
+ Assert.Equal(2, propertyChangedCount);
+
+ cancellationTokenSource.Cancel();
+ animationRun.Wait();
+ }
}
}
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/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs
new file mode 100644
index 0000000000..7626be7760
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs
@@ -0,0 +1,103 @@
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Utilities;
+using Avalonia.Visuals.Media.Imaging;
+
+namespace Avalonia.Benchmarks
+{
+ internal class NullDrawingContextImpl : IDrawingContextImpl
+ {
+ public void Dispose()
+ {
+ }
+
+ public Matrix Transform { get; set; }
+
+ public void Clear(Color color)
+ {
+ }
+
+ public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect,
+ BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
+ {
+ }
+
+ public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
+ {
+ }
+
+ public void DrawLine(IPen pen, Point p1, Point p2)
+ {
+ }
+
+ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
+ {
+ }
+
+ public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default)
+ {
+ }
+
+ public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
+ {
+ }
+
+ public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
+ {
+ }
+
+ public IDrawingContextLayerImpl CreateLayer(Size size)
+ {
+ return null;
+ }
+
+ public void PushClip(Rect clip)
+ {
+ }
+
+ public void PushClip(RoundedRect clip)
+ {
+ }
+
+ public void PopClip()
+ {
+ }
+
+ public void PushOpacity(double opacity)
+ {
+ }
+
+ public void PopOpacity()
+ {
+ }
+
+ public void PushOpacityMask(IBrush mask, Rect bounds)
+ {
+ }
+
+ public void PopOpacityMask()
+ {
+ }
+
+ public void PushGeometryClip(IGeometryImpl clip)
+ {
+ }
+
+ public void PopGeometryClip()
+ {
+ }
+
+ public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
+ {
+ }
+
+ public void PopBitmapBlendMode()
+ {
+ }
+
+ public void Custom(ICustomDrawOperation custom)
+ {
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
index 268977d662..876a0de643 100644
--- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
+++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
@@ -18,12 +18,12 @@ namespace Avalonia.Benchmarks
public IGeometryImpl CreateEllipseGeometry(Rect rect)
{
- throw new NotImplementedException();
+ return new MockStreamGeometryImpl();
}
public IGeometryImpl CreateLineGeometry(Point p1, Point p2)
{
- throw new NotImplementedException();
+ return new MockStreamGeometryImpl();
}
public IGeometryImpl CreateRectangleGeometry(Rect rect)
@@ -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();
diff --git a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs
new file mode 100644
index 0000000000..b0db806afa
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs
@@ -0,0 +1,53 @@
+using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using Avalonia.Platform;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Rendering
+{
+ [MemoryDiagnoser]
+ public class ShapeRendering
+ {
+ private readonly DrawingContext _drawingContext;
+ private readonly Line _lineFill;
+ private readonly Line _lineFillAndStroke;
+ private readonly Line _lineNoBrushes;
+ private readonly Line _lineStroke;
+
+ public ShapeRendering()
+ {
+ _lineNoBrushes = new Line();
+ _lineStroke = new Line { Stroke = new SolidColorBrush() };
+ _lineFill = new Line { Fill = new SolidColorBrush() };
+ _lineFillAndStroke = new Line { Stroke = new SolidColorBrush(), Fill = new SolidColorBrush() };
+
+ _drawingContext = new DrawingContext(new NullDrawingContextImpl(), true);
+
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(new NullRenderingPlatform());
+ }
+
+ [Benchmark]
+ public void Render_Line_NoBrushes()
+ {
+ _lineNoBrushes.Render(_drawingContext);
+ }
+
+ [Benchmark]
+ public void Render_Line_WithStroke()
+ {
+ _lineStroke.Render(_drawingContext);
+ }
+
+ [Benchmark]
+ public void Render_Line_WithFill()
+ {
+ _lineFill.Render(_drawingContext);
+ }
+
+ [Benchmark]
+ public void Render_Line_WithFillAndStroke()
+ {
+ _lineFillAndStroke.Render(_drawingContext);
+ }
+ }
+}
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/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
index 8f9c7fdb0b..cb2fd11175 100644
--- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
@@ -201,6 +201,65 @@ namespace Avalonia.Controls.UnitTests
}
- }
+ }
+
+ [Fact]
+ public void Close_Window_On_Alt_F4_When_ComboBox_Is_Focus()
+ {
+ var inputManagerMock = new Moq.Mock();
+ var services = TestServices.StyledWindow.With(inputManager: inputManagerMock.Object);
+
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var window = new Window();
+
+ window.KeyDown += (s, e) =>
+ {
+ if (e.Handled == false
+ && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) == true
+ && e.Key == Key.F4 )
+ {
+ e.Handled = true;
+ window.Close();
+ }
+ };
+
+ var count = 0;
+
+ var target = new ComboBox
+ {
+ Items = new[] { new Canvas() },
+ SelectedIndex = 0,
+ Template = GetTemplate(),
+ };
+
+ window.Content = target;
+
+
+ window.Closing +=
+ (sender, e) =>
+ {
+ count++;
+ };
+
+ window.Show();
+
+ target.Focus();
+
+ _helper.Down(target);
+ _helper.Up(target);
+ Assert.True(target.IsDropDownOpen);
+
+ target.RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = InputElement.KeyDownEvent,
+ KeyModifiers = KeyModifiers.Alt,
+ Key = Key.F4
+ });
+
+
+ Assert.Equal(1, count);
+ }
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
index cea77bb7c9..72ba3ab273 100644
--- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
@@ -1066,7 +1066,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection()
{
- /// Issue #2980.
+ // Issue #2980.
using (Application())
{
var target = new DerivedTreeView
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.Input.UnitTests/KeyboardDeviceTests.cs b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
index df0a077c7f..7730cee78c 100644
--- a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
+++ b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
@@ -1,5 +1,9 @@
-using Avalonia.Input.Raw;
+using System;
+using System.Windows.Input;
+using Avalonia.Controls;
+using Avalonia.Input.Raw;
using Avalonia.Interactivity;
+using Avalonia.UnitTests;
using Moq;
using Xunit;
@@ -86,5 +90,45 @@ namespace Avalonia.Input.UnitTests
focused.Verify(x => x.RaiseEvent(It.IsAny()));
}
+
+ [Fact]
+ public void Can_Change_KeyBindings_In_Keybinding_Event_Handler()
+ {
+ var target = new KeyboardDevice();
+ var button = new Button();
+ var root = new TestRoot(button);
+ var raised = 0;
+
+ button.KeyBindings.Add(new KeyBinding
+ {
+ Gesture = new KeyGesture(Key.O, KeyModifiers.Control),
+ Command = new DelegateCommand(() =>
+ {
+ button.KeyBindings.Clear();
+ ++raised;
+ }),
+ });
+
+ target.SetFocusedElement(button, NavigationMethod.Pointer, 0);
+ target.ProcessRawEvent(
+ new RawKeyEventArgs(
+ target,
+ 0,
+ root,
+ RawKeyEventType.KeyDown,
+ Key.O,
+ RawInputModifiers.Control));
+
+ Assert.Equal(1, raised);
+ }
+
+ private class DelegateCommand : ICommand
+ {
+ private readonly Action _action;
+ public DelegateCommand(Action action) => _action = action;
+ public event EventHandler CanExecuteChanged;
+ public bool CanExecute(object parameter) => true;
+ public void Execute(object parameter) => _action();
+ }
}
}
diff --git a/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs
index db69a0a028..3d2f09e2a8 100644
--- a/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs
+++ b/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs
@@ -1,5 +1,6 @@
using Avalonia.Controls;
using Avalonia.Media;
+using System;
using System.Threading.Tasks;
using Xunit;
@@ -174,5 +175,44 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target);
CompareImages();
}
+
+ [Fact]
+ public async Task ConicGradientBrush_DrawingContext()
+ {
+ var brush = new ConicGradientBrush
+ {
+ GradientStops =
+ {
+ new GradientStop { Color = Colors.Red, Offset = 0 },
+ new GradientStop { Color = Colors.Yellow, Offset = 0.1667 },
+ new GradientStop { Color = Colors.Lime, Offset = 0.3333 },
+ new GradientStop { Color = Colors.Aqua, Offset = 0.5000 },
+ new GradientStop { Color = Colors.Blue, Offset = 0.6667 },
+ new GradientStop { Color = Colors.Magenta, Offset = 0.8333 },
+ new GradientStop { Color = Colors.Red, Offset = 1 },
+ }
+ };
+
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new DrawnControl(c =>
+ {
+ c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
+ c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100));
+ }),
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+
+ private class DrawnControl : Control
+ {
+ private readonly Action _render;
+ public DrawnControl(Action render) => _render = render;
+ public override void Render(DrawingContext context) => _render(context);
+ }
}
}
diff --git a/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs b/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs
new file mode 100644
index 0000000000..5ca21a3535
--- /dev/null
+++ b/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs
@@ -0,0 +1,84 @@
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Xunit;
+
+#if AVALONIA_SKIA
+namespace Avalonia.Skia.RenderTests
+#else
+namespace Avalonia.Direct2D1.RenderTests.Media
+#endif
+{
+ public class ImageDrawingTests : TestBase
+ {
+ public ImageDrawingTests()
+ : base(@"Media\ImageDrawing")
+ {
+ }
+
+ private string BitmapPath
+ {
+ get { return System.IO.Path.Combine(OutputPath, "github_icon.png"); }
+ }
+
+ [Fact]
+ public async Task ImageDrawing_Fill()
+ {
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Image
+ {
+ Source = new DrawingImage
+ {
+ Drawing = new ImageDrawing
+ {
+ ImageSource = new Bitmap(BitmapPath),
+ Rect = new Rect(0, 0, 200, 200),
+ }
+ }
+ }
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+
+ [Fact]
+ public async Task ImageDrawing_BottomRight()
+ {
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Image
+ {
+ Source = new DrawingImage
+ {
+ Drawing = new DrawingGroup
+ {
+ Children =
+ {
+ new GeometryDrawing
+ {
+ Geometry = StreamGeometry.Parse("m0,0 l200,200"),
+ Brush = Brushes.Black,
+ },
+ new ImageDrawing
+ {
+ ImageSource = new Bitmap(BitmapPath),
+ Rect = new Rect(100, 100, 100, 100),
+ }
+ }
+ }
+ }
+ }
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+ }
+}
diff --git a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
index af7b4f130f..28701f2f97 100644
--- a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
+++ b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
@@ -72,5 +72,41 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target);
CompareImages();
}
+
+ [Fact]
+ public async Task LinearGradientBrush_DrawingContext()
+ {
+ var brush = new LinearGradientBrush
+ {
+ StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
+ EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
+ GradientStops =
+ {
+ new GradientStop { Color = Colors.Red, Offset = 0 },
+ new GradientStop { Color = Colors.Blue, Offset = 1 }
+ }
+ };
+
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new DrawnControl(c =>
+ {
+ c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
+ c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100));
+ }),
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+
+ private class DrawnControl : Control
+ {
+ private readonly Action _render;
+ public DrawnControl(Action render) => _render = render;
+ public override void Render(DrawingContext context) => _render(context);
+ }
}
}
diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
index 2941b8dc34..95fae7f2fa 100644
--- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
+++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
@@ -165,5 +165,39 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target);
CompareImages();
}
+
+ [Fact]
+ public async Task RadialGradientBrush_DrawingContext()
+ {
+ var brush = new RadialGradientBrush
+ {
+ GradientStops =
+ {
+ new GradientStop { Color = Colors.Red, Offset = 0 },
+ new GradientStop { Color = Colors.Blue, Offset = 1 }
+ }
+ };
+
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new DrawnControl(c =>
+ {
+ c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
+ c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100));
+ }),
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+
+ private class DrawnControl : Control
+ {
+ private readonly Action _render;
+ public DrawnControl(Action render) => _render = render;
+ public override void Render(DrawingContext context) => _render(context);
+ }
}
}
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/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index 05dd32b84d..9c2a1953f1 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -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()
{
@@ -401,6 +422,24 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
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()
{
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.Styling.UnitTests/SelectorTests_PropertyEquals.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs
index e149410152..7689a458ae 100644
--- a/tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs
+++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_PropertyEquals.cs
@@ -22,6 +22,23 @@ namespace Avalonia.Styling.UnitTests
Assert.False(await activator.Take(1));
}
+ [Theory]
+ [InlineData("Bar", FooBar.Bar)]
+ [InlineData("352", 352)]
+ [InlineData("0.1", 0.1)]
+ public async Task PropertyEquals_Matches_When_Property_Has_Matching_Value_And_Different_Type(string literal, object value)
+ {
+ var control = new TextBlock();
+ var target = default(Selector).PropertyEquals(TextBlock.TagProperty, literal);
+ var activator = target.Match(control).Activator.ToObservable();
+
+ Assert.False(await activator.Take(1));
+ control.Tag = value;
+ Assert.True(await activator.Take(1));
+ control.Tag = null;
+ Assert.False(await activator.Take(1));
+ }
+
[Fact]
public void OfType_PropertyEquals_Doesnt_Match_Control_Of_Wrong_Type()
{
@@ -40,5 +57,11 @@ namespace Avalonia.Styling.UnitTests
Assert.Equal("TextBlock[Text=foo]", target.ToString());
}
+
+ private enum FooBar
+ {
+ Foo,
+ Bar
+ }
}
}
diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
index 8015172e72..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();
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs
index c2a1a5f9e4..8c25019606 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs
@@ -59,9 +59,9 @@ namespace Avalonia.Visuals.UnitTests.Media
}
[Fact]
- public void Equality_Is_Implemented_Between_Immutable_And_Mmutable_Pens()
+ public void Equality_Is_Implemented_Between_Immutable_And_Mutable_Pens()
{
- var brush = new SolidColorBrush(Colors.Red);
+ var brush = new ImmutableSolidColorBrush(Colors.Red);
var target1 = new ImmutablePen(
brush: brush,
thickness: 2,
@@ -83,7 +83,7 @@ namespace Avalonia.Visuals.UnitTests.Media
[Fact]
public void Equality_Is_Implemented_Between_Mutable_And_Immutable_DashStyles()
{
- var brush = new SolidColorBrush(Colors.Red);
+ var brush = new ImmutableSolidColorBrush(Colors.Red);
var target1 = new ImmutablePen(
brush: brush,
thickness: 2,
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/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
index 1a6d003062..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();
diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png
new file mode 100644
index 0000000000..3a2241b53f
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_BottomRight.expected.png b/tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_BottomRight.expected.png
new file mode 100644
index 0000000000..c6b7c2f307
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_BottomRight.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_Fill.expected.png b/tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_Fill.expected.png
new file mode 100644
index 0000000000..acc8532ff9
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ImageDrawing/ImageDrawing_Fill.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/ImageDrawing/github_icon.png b/tests/TestFiles/Direct2D1/Media/ImageDrawing/github_icon.png
new file mode 100644
index 0000000000..cd053c5fe1
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ImageDrawing/github_icon.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/LinearGradientBrush/LinearGradientBrush_DrawingContext.expected.png b/tests/TestFiles/Direct2D1/Media/LinearGradientBrush/LinearGradientBrush_DrawingContext.expected.png
new file mode 100644
index 0000000000..7ba0bf1622
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/LinearGradientBrush/LinearGradientBrush_DrawingContext.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png
new file mode 100644
index 0000000000..85d11f2c2b
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png
new file mode 100644
index 0000000000..3906d5a181
Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_DrawingContext.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_BottomRight.expected.png b/tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_BottomRight.expected.png
new file mode 100644
index 0000000000..f7a2510551
Binary files /dev/null and b/tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_BottomRight.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_Fill.expected.png b/tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_Fill.expected.png
new file mode 100644
index 0000000000..fe99db43aa
Binary files /dev/null and b/tests/TestFiles/Skia/Media/ImageDrawing/ImageDrawing_Fill.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/ImageDrawing/github_icon.png b/tests/TestFiles/Skia/Media/ImageDrawing/github_icon.png
new file mode 100644
index 0000000000..cd053c5fe1
Binary files /dev/null and b/tests/TestFiles/Skia/Media/ImageDrawing/github_icon.png differ
diff --git a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_DrawingContext.expected.png b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_DrawingContext.expected.png
new file mode 100644
index 0000000000..5ccfc0fcfa
Binary files /dev/null and b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_DrawingContext.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png
new file mode 100644
index 0000000000..55533a5828
Binary files /dev/null and b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_DrawingContext.expected.png differ