diff --git a/readme.md b/readme.md
index 8ae3f1ad66..9d317cdd06 100644
--- a/readme.md
+++ b/readme.md
@@ -1,24 +1,20 @@
-
+[](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [](#backers) [](#sponsors) 
+
+[](https://www.nuget.org/packages/Avalonia) [](https://www.nuget.org/packages/Avalonia) [](https://www.myget.org/gallery/avalonia-ci) 
-# Avalonia
+
-| Gitter Chat | Build Status (Win, Linux, OSX) | Open Collective | NuGet | MyGet |
-|---|---|---|---|---|
-| [](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) | [](#backers) [](#sponsors) | [](https://www.nuget.org/packages/Avalonia) | [](https://www.myget.org/gallery/avalonia-ci) |
+## 📖 About AvaloniaUI
-## About
+Avalonia is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows via .NET Framework and .NET Core, Linux via Xorg, macOS. Avalonia is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development.
-**Avalonia** is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS.
+
-**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development.
+> **Note:** The UI theme you see in the picture above is still work-in-progress and will be available in the upcoming Avalonia 0.10.0 release. However, you can connect to our nightly build feed and install latest pre-release versions of Avalonia NuGet packages, if you are willing to help out with the development and testing. See [Using nightly build feed](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) for more info.
-To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239).
+To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. [Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia!
-You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been.
-
-[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia!
-
-## Getting Started
+## 🚀 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 starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project).
@@ -30,6 +26,15 @@ Install-Package Avalonia
Install-Package Avalonia.Desktop
```
+## Showcase
+
+Examples of UIs built with Avalonia
+
+
+
+
+
+
## JetBrains Rider
If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen.
diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml
index 20ca291910..c7a75f5a70 100644
--- a/samples/ControlCatalog/App.xaml
+++ b/samples/ControlCatalog/App.xaml
@@ -1,10 +1,8 @@
-
-
-
-
+
+
-
+
+
+
+
+
No Border
diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs
index 1d0c228a0e..5e555c8c91 100644
--- a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ButtonPage.xaml.cs
@@ -5,5 +5,25 @@ namespace ControlCatalog.Pages
{
public class ButtonPage : UserControl
{
+ private int repeatButtonClickCount = 0;
+
+ public ButtonPage()
+ {
+ InitializeComponent();
+
+ this.FindControl("RepeatButton").Click += OnRepeatButtonClick;
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public void OnRepeatButtonClick(object sender, object args)
+ {
+ repeatButtonClickCount++;
+ var textBlock = this.FindControl("RepeatButtonTextBlock");
+ textBlock.Text = $"Repeat Button: {repeatButtonClickCount}";
+ }
}
}
diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
similarity index 75%
rename from samples/ControlCatalog/Pages/DatePickerPage.xaml
rename to samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
index 30d5a7506f..107472105a 100644
--- a/samples/ControlCatalog/Pages/DatePickerPage.xaml
+++ b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
@@ -1,8 +1,8 @@
+ x:Class="ControlCatalog.Pages.CalendarDatePickerPage">
- DatePicker
+ CalendarDatePicker
A control for selecting dates with a calendar drop-down
-
-
-
-
-
-
-
+
diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml.cs b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs
similarity index 57%
rename from samples/ControlCatalog/Pages/DatePickerPage.xaml.cs
rename to samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs
index ef01887c9e..95bdeb363a 100644
--- a/samples/ControlCatalog/Pages/DatePickerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs
@@ -4,17 +4,17 @@ using System;
namespace ControlCatalog.Pages
{
- public class DatePickerPage : UserControl
+ public class CalendarDatePickerPage : UserControl
{
- public DatePickerPage()
+ public CalendarDatePickerPage()
{
InitializeComponent();
- var dp1 = this.FindControl("DatePicker1");
- var dp2 = this.FindControl("DatePicker2");
- var dp3 = this.FindControl("DatePicker3");
- var dp4 = this.FindControl("DatePicker4");
- var dp5 = this.FindControl("DatePicker5");
+ var dp1 = this.FindControl("DatePicker1");
+ var dp2 = this.FindControl("DatePicker2");
+ var dp3 = this.FindControl("DatePicker3");
+ var dp4 = this.FindControl("DatePicker4");
+ var dp5 = this.FindControl("DatePicker5");
dp1.SelectedDate = DateTime.Today;
dp2.SelectedDate = DateTime.Today.AddDays(10);
diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml
index 0834e829d8..a0e82663bf 100644
--- a/samples/ControlCatalog/Pages/DialogsPage.xaml
+++ b/samples/ControlCatalog/Pages/DialogsPage.xaml
@@ -11,5 +11,7 @@
Decorated window (dialog)
Dialog
Dialog (No taskbar icon)
+ Owned window
+ Owned window (No taskbar icon)
diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
index dcb94a89e7..cf6c771e34 100644
--- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
@@ -93,6 +93,21 @@ namespace ControlCatalog.Pages
window.ShowInTaskbar = false;
window.ShowDialog(GetWindow());
};
+ this.FindControl("OwnedWindow").Click += delegate
+ {
+ var window = CreateSampleWindow();
+
+ window.Show(GetWindow());
+ };
+
+ this.FindControl("OwnedWindowNoTaskbar").Click += delegate
+ {
+ var window = CreateSampleWindow();
+
+ window.ShowInTaskbar = false;
+
+ window.Show(GetWindow());
+ };
}
private Window CreateSampleWindow()
diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml
index e605a92da0..0d7e5da17f 100644
--- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml
+++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml
@@ -50,22 +50,22 @@
Text:
-
+
Minimum:
+ CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" HorizontalAlignment="Center"/>
Maximum:
+ CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" HorizontalAlignment="Center"/>
Increment:
+ Margin="2" HorizontalAlignment="Center"/>
Value:
+ Margin="2" HorizontalAlignment="Center"/>
@@ -73,7 +73,7 @@
Usage of NumericUpDown:
diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml
index 58f7b881fe..c6f5521e60 100644
--- a/samples/ControlCatalog/Pages/SliderPage.xaml
+++ b/samples/ControlCatalog/Pages/SliderPage.xaml
@@ -9,12 +9,14 @@
diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml
index cbe1e3059c..73d83e08f1 100644
--- a/samples/ControlCatalog/Pages/ToolTipPage.xaml
+++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml
@@ -18,7 +18,7 @@
ToolTip.Tip="This is a ToolTip">
Hover Here
- Add
Remove
+ Select Random
Single
diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
index d396ef2b3d..5bc23e2fe5 100644
--- a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
@@ -23,12 +23,14 @@ namespace ControlCatalog.ViewModels
AddItemCommand = ReactiveCommand.Create(AddItem);
RemoveItemCommand = ReactiveCommand.Create(RemoveItem);
+ SelectRandomItemCommand = ReactiveCommand.Create(SelectRandomItem);
}
public ObservableCollection Items { get; }
public SelectionModel Selection { get; }
public ReactiveCommand AddItemCommand { get; }
public ReactiveCommand RemoveItemCommand { get; }
+ public ReactiveCommand SelectRandomItemCommand { get; }
public SelectionMode SelectionMode
{
@@ -74,6 +76,15 @@ namespace ControlCatalog.ViewModels
}
}
+ private void SelectRandomItem()
+ {
+ var random = new Random();
+ var depth = random.Next(4);
+ var indexes = Enumerable.Range(0, 4).Select(x => random.Next(10));
+ var path = new IndexPath(indexes);
+ Selection.SelectedIndex = path;
+ }
+
private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
var selected = string.Join(",", e.SelectedIndices);
diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index c098ef411e..14ccc82043 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -29,6 +29,9 @@
+
+
+
diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml
new file mode 100644
index 0000000000..d6da293ff3
--- /dev/null
+++ b/samples/RenderDemo/Pages/TransitionsPage.xaml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hover to activate Transform Keyframe Animations.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml.cs b/samples/RenderDemo/Pages/TransitionsPage.xaml.cs
new file mode 100644
index 0000000000..5f446c9e99
--- /dev/null
+++ b/samples/RenderDemo/Pages/TransitionsPage.xaml.cs
@@ -0,0 +1,37 @@
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using RenderDemo.ViewModels;
+
+namespace RenderDemo.Pages
+{
+ public class TransitionsPage : UserControl
+ {
+ public TransitionsPage()
+ {
+ InitializeComponent();
+ this.DataContext = new AnimationsPageViewModel();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void ToggleClock(object sender, RoutedEventArgs args)
+ {
+ var button = sender as Button;
+ var clock = button.Clock;
+
+ if (clock.PlayState == PlayState.Run)
+ {
+ clock.PlayState = PlayState.Pause;
+ }
+ else if (clock.PlayState == PlayState.Pause)
+ {
+ clock.PlayState = PlayState.Run;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs
index ad2001d621..5184341324 100644
--- a/src/Avalonia.Animation/TransitionInstance.cs
+++ b/src/Avalonia.Animation/TransitionInstance.cs
@@ -4,6 +4,7 @@ using System.Reactive.Linq;
using Avalonia.Animation.Easings;
using Avalonia.Animation.Utils;
using Avalonia.Reactive;
+using Avalonia.Utilities;
namespace Avalonia.Animation
{
@@ -13,31 +14,56 @@ namespace Avalonia.Animation
internal class TransitionInstance : SingleSubscriberObservableBase
{
private IDisposable _timerSubscription;
+ private TimeSpan _delay;
private TimeSpan _duration;
private readonly IClock _baseClock;
private IClock _clock;
- public TransitionInstance(IClock clock, TimeSpan Duration)
+ public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration)
{
clock = clock ?? throw new ArgumentNullException(nameof(clock));
- _duration = Duration;
+ _delay = delay;
+ _duration = duration;
_baseClock = clock;
}
private void TimerTick(TimeSpan t)
{
- var interpVal = _duration.Ticks == 0 ? 1d : (double)t.Ticks / _duration.Ticks;
+
+ // [<------------- normalizedTotalDur ------------------>]
+ // [<---- Delay ---->][<---------- Duration ------------>]
+ // ^- normalizedDelayEnd
+ // [<---- normalizedInterpVal --->]
+
+ var normalizedInterpVal = 1d;
+
+ if (!MathUtilities.AreClose(_duration.TotalSeconds, 0d))
+ {
+ var normalizedTotalDur = _delay + _duration;
+ var normalizedDelayEnd = _delay.TotalSeconds / normalizedTotalDur.TotalSeconds;
+ var normalizedPresentationTime = t.TotalSeconds / normalizedTotalDur.TotalSeconds;
+
+ if (normalizedPresentationTime < normalizedDelayEnd
+ || MathUtilities.AreClose(normalizedPresentationTime, normalizedDelayEnd))
+ {
+ normalizedInterpVal = 0d;
+ }
+ else
+ {
+ normalizedInterpVal = (t.TotalSeconds - _delay.TotalSeconds) / _duration.TotalSeconds;
+ }
+ }
// Clamp interpolation value.
- if (interpVal >= 1d | interpVal < 0d)
+ if (normalizedInterpVal >= 1d || normalizedInterpVal < 0d)
{
PublishNext(1d);
PublishCompleted();
}
else
{
- PublishNext(interpVal);
+ PublishNext(normalizedInterpVal);
}
}
diff --git a/src/Avalonia.Animation/Transition`1.cs b/src/Avalonia.Animation/Transition`1.cs
index 138131acb9..4542a137e5 100644
--- a/src/Avalonia.Animation/Transition`1.cs
+++ b/src/Avalonia.Animation/Transition`1.cs
@@ -13,10 +13,15 @@ namespace Avalonia.Animation
private AvaloniaProperty _prop;
///
- /// Gets the duration of the animation.
+ /// Gets or sets the duration of the transition.
///
public TimeSpan Duration { get; set; }
+ ///
+ /// Gets or sets delay before starting the transition.
+ ///
+ public TimeSpan Delay { get; set; } = TimeSpan.Zero;
+
///
/// Gets the easing class to be used.
///
@@ -47,7 +52,7 @@ namespace Avalonia.Animation
///
public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue)
{
- var transition = DoTransition(new TransitionInstance(clock, Duration), (T)oldValue, (T)newValue);
+ var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue, (T)newValue);
return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation);
}
}
diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
index 84ef0fb695..8fc2a7b77c 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Data.Core.Plugins
public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
{
///
- public bool Match(object obj, string propertyName) => true;
+ public bool Match(object obj, string propertyName) => GetPropertyWithName(obj.GetType(), propertyName) != null;
///
/// Starts monitoring the value of a property on an object.
@@ -30,10 +30,7 @@ namespace Avalonia.Data.Core.Plugins
reference.TryGetTarget(out object instance);
- const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public |
- BindingFlags.Static | BindingFlags.Instance;
-
- var p = instance.GetType().GetProperty(propertyName, bindingFlags);
+ var p = GetPropertyWithName(instance.GetType(), propertyName);
if (p != null)
{
@@ -47,6 +44,14 @@ namespace Avalonia.Data.Core.Plugins
}
}
+ private static PropertyInfo GetPropertyWithName(Type type, string propertyName)
+ {
+ const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public |
+ BindingFlags.Static | BindingFlags.Instance;
+
+ return type.GetProperty(propertyName, bindingFlags);
+ }
+
private class Accessor : PropertyAccessorBase, IWeakSubscriber
{
private readonly WeakReference _reference;
diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
index e20685b1dd..a0868152f6 100644
--- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
+++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
@@ -62,8 +62,13 @@ namespace Avalonia.Data.Core
if (accessor == null)
{
- throw new NotSupportedException(
- $"Could not find a matching property accessor for {PropertyName}.");
+ reference.TryGetTarget(out object instance);
+
+ var message = $"Could not find a matching property accessor for '{PropertyName}' on '{instance}'";
+
+ var exception = new MissingMemberException(message);
+
+ accessor = new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
_accessor = accessor;
diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs
index 75d58f45d5..692982cdc6 100644
--- a/src/Avalonia.Base/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs
@@ -7,4 +7,5 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")]
[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.UnitTests")]
-[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
\ No newline at end of file
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
+[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")]
diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs
index a1a2d6c3d0..06a1cd4ae5 100644
--- a/src/Avalonia.Base/Utilities/MathUtilities.cs
+++ b/src/Avalonia.Base/Utilities/MathUtilities.cs
@@ -1,5 +1,4 @@
using System;
-using System.Runtime.InteropServices;
namespace Avalonia.Utilities
{
@@ -8,6 +7,11 @@ namespace Avalonia.Utilities
///
public static class MathUtilities
{
+ // smallest such that 1.0+DoubleEpsilon != 1.0
+ internal static readonly double DoubleEpsilon = 2.2204460492503131e-016;
+
+ private const float FloatEpsilon = 1.192092896e-07F;
+
///
/// AreClose - Returns whether or not two doubles are "close". That is, whether or
/// not they are within epsilon of each other.
@@ -18,11 +22,26 @@ namespace Avalonia.Utilities
{
//in case they are Infinities (then epsilon check does not work)
if (value1 == value2) return true;
- double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon;
+ double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DoubleEpsilon;
double delta = value1 - value2;
return (-eps < delta) && (eps > delta);
}
+ ///
+ /// AreClose - Returns whether or not two floats are "close". That is, whether or
+ /// not they are within epsilon of each other.
+ ///
+ /// The first float to compare.
+ /// The second float to compare.
+ public static bool AreClose(float value1, float value2)
+ {
+ //in case they are Infinities (then epsilon check does not work)
+ if (value1 == value2) return true;
+ float eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0f) * FloatEpsilon;
+ float delta = value1 - value2;
+ return (-eps < delta) && (eps > delta);
+ }
+
///
/// LessThan - Returns whether or not the first double is less than the second double.
/// That is, whether or not the first is strictly less than *and* not within epsilon of
@@ -35,6 +54,18 @@ namespace Avalonia.Utilities
return (value1 < value2) && !AreClose(value1, value2);
}
+ ///
+ /// LessThan - Returns whether or not the first float is less than the second float.
+ /// That is, whether or not the first is strictly less than *and* not within epsilon of
+ /// the other number.
+ ///
+ /// The first single float to compare.
+ /// The second single float to compare.
+ public static bool LessThan(float value1, float value2)
+ {
+ return (value1 < value2) && !AreClose(value1, value2);
+ }
+
///
/// GreaterThan - Returns whether or not the first double is greater than the second double.
/// That is, whether or not the first is strictly greater than *and* not within epsilon of
@@ -47,6 +78,18 @@ namespace Avalonia.Utilities
return (value1 > value2) && !AreClose(value1, value2);
}
+ ///
+ /// GreaterThan - Returns whether or not the first float is greater than the second float.
+ /// That is, whether or not the first is strictly greater than *and* not within epsilon of
+ /// the other number.
+ ///
+ /// The first float to compare.
+ /// The second float to compare.
+ public static bool GreaterThan(float value1, float value2)
+ {
+ return (value1 > value2) && !AreClose(value1, value2);
+ }
+
///
/// LessThanOrClose - Returns whether or not the first double is less than or close to
/// the second double. That is, whether or not the first is strictly less than or within
@@ -59,6 +102,18 @@ namespace Avalonia.Utilities
return (value1 < value2) || AreClose(value1, value2);
}
+ ///
+ /// LessThanOrClose - Returns whether or not the first float is less than or close to
+ /// the second float. That is, whether or not the first is strictly less than or within
+ /// epsilon of the other number.
+ ///
+ /// The first float to compare.
+ /// The second float to compare.
+ public static bool LessThanOrClose(float value1, float value2)
+ {
+ return (value1 < value2) || AreClose(value1, value2);
+ }
+
///
/// GreaterThanOrClose - Returns whether or not the first double is greater than or close to
/// the second double. That is, whether or not the first is strictly greater than or within
@@ -71,6 +126,18 @@ namespace Avalonia.Utilities
return (value1 > value2) || AreClose(value1, value2);
}
+ ///
+ /// GreaterThanOrClose - Returns whether or not the first float is greater than or close to
+ /// the second float. That is, whether or not the first is strictly greater than or within
+ /// epsilon of the other number.
+ ///
+ /// The first float to compare.
+ /// The second float to compare.
+ public static bool GreaterThanOrClose(float value1, float value2)
+ {
+ return (value1 > value2) || AreClose(value1, value2);
+ }
+
///
/// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1),
/// but this is faster.
@@ -78,7 +145,17 @@ namespace Avalonia.Utilities
/// The double to compare to 1.
public static bool IsOne(double value)
{
- return Math.Abs(value - 1.0) < 10.0 * double.Epsilon;
+ return Math.Abs(value - 1.0) < 10.0 * DoubleEpsilon;
+ }
+
+ ///
+ /// IsOne - Returns whether or not the float is "close" to 1. Same as AreClose(float, 1),
+ /// but this is faster.
+ ///
+ /// The float to compare to 1.
+ public static bool IsOne(float value)
+ {
+ return Math.Abs(value - 1.0f) < 10.0f * FloatEpsilon;
}
///
@@ -88,7 +165,17 @@ namespace Avalonia.Utilities
/// The double to compare to 0.
public static bool IsZero(double value)
{
- return Math.Abs(value) < 10.0 * double.Epsilon;
+ return Math.Abs(value) < 10.0 * DoubleEpsilon;
+ }
+
+ ///
+ /// IsZero - Returns whether or not the float is "close" to 0. Same as AreClose(float, 0),
+ /// but this is faster.
+ ///
+ /// The float to compare to 0.
+ public static bool IsZero(float value)
+ {
+ return Math.Abs(value) < 10.0f * FloatEpsilon;
}
///
@@ -100,6 +187,11 @@ namespace Avalonia.Utilities
/// The clamped value.
public static double Clamp(double val, double min, double max)
{
+ if (min > max)
+ {
+ ThrowCannotBeGreaterThanException(min, max);
+ }
+
if (val < min)
{
return min;
@@ -128,7 +220,7 @@ namespace Avalonia.Utilities
double newValue;
// If DPI == 1, don't use DPI-aware rounding.
- if (!MathUtilities.AreClose(dpiScale, 1.0))
+ if (!MathUtilities.IsOne(dpiScale))
{
newValue = Math.Round(value * dpiScale) / dpiScale;
// If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value.
@@ -158,7 +250,7 @@ namespace Avalonia.Utilities
{
if (min > max)
{
- throw new ArgumentException($"{min} cannot be greater than {max}.");
+ ThrowCannotBeGreaterThanException(min, max);
}
if (val < min)
@@ -174,5 +266,40 @@ namespace Avalonia.Utilities
return val;
}
}
+
+ ///
+ /// Converts an angle in degrees to radians.
+ ///
+ /// The angle in degrees.
+ /// The angle in radians.
+ public static double Deg2Rad(double angle)
+ {
+ return angle * (Math.PI / 180d);
+ }
+
+ ///
+ /// Converts an angle in gradians to radians.
+ ///
+ /// The angle in gradians.
+ /// The angle in radians.
+ public static double Grad2Rad(double angle)
+ {
+ return angle * (Math.PI / 200d);
+ }
+
+ ///
+ /// Converts an angle in turns to radians.
+ ///
+ /// The angle in turns.
+ /// The angle in radians.
+ public static double Turn2Rad(double angle)
+ {
+ return angle * 2 * Math.PI;
+ }
+
+ private static void ThrowCannotBeGreaterThanException(double min, double max)
+ {
+ throw new ArgumentException($"{min} cannot be greater than {max}.");
+ }
}
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index cfe47a09d5..7e893d131e 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -2681,7 +2681,7 @@ namespace Avalonia.Controls
{
return;
}
- Debug.Assert(DoubleUtil.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum));
+ Debug.Assert(MathUtilities.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum));
_verticalScrollChangesIgnored++;
try
@@ -2698,7 +2698,7 @@ namespace Avalonia.Controls
}
else if (scrollEventType == ScrollEventType.SmallDecrement)
{
- if (DoubleUtil.GreaterThan(NegVerticalOffset, 0))
+ if (MathUtilities.GreaterThan(NegVerticalOffset, 0))
{
DisplayData.PendingVerticalScrollHeight -= NegVerticalOffset;
}
@@ -2717,7 +2717,7 @@ namespace Avalonia.Controls
DisplayData.PendingVerticalScrollHeight = _vScrollBar.Value - _verticalOffset;
}
- if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight))
+ if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight))
{
// Invalidate so the scroll happens on idle
InvalidateRowsMeasure(invalidateIndividualElements: false);
@@ -3346,22 +3346,22 @@ namespace Avalonia.Controls
bool needHorizScrollbarWithoutVertScrollbar = false;
if (allowHorizScrollbar &&
- DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) &&
- DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
- DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight))
+ MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
+ MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
+ MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight))
{
double oldDataHeight = cellsHeight;
cellsHeight -= horizScrollBarHeight;
Debug.Assert(cellsHeight >= 0);
needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true;
- if (allowVertScrollbar && (DoubleUtil.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) ||
- DoubleUtil.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth)))
+ if (allowVertScrollbar && (MathUtilities.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) ||
+ MathUtilities.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth)))
{
// Would we still need a horizontal scrollbar without the vertical one?
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
if (DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{
- needHorizScrollbar = DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth);
+ needHorizScrollbar = MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth);
}
}
@@ -3374,8 +3374,8 @@ namespace Avalonia.Controls
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
if (allowVertScrollbar &&
- DoubleUtil.GreaterThan(cellsHeight, 0) &&
- DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
+ MathUtilities.GreaterThan(cellsHeight, 0) &&
+ MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{
cellsWidth -= vertScrollBarWidth;
@@ -3389,9 +3389,9 @@ namespace Avalonia.Controls
if (allowHorizScrollbar &&
needVertScrollbar && !needHorizScrollbar &&
- DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) &&
- DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
- DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight))
+ MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
+ MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
+ MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight))
{
cellsWidth += vertScrollBarWidth;
cellsHeight -= horizScrollBarHeight;
@@ -3422,7 +3422,7 @@ namespace Avalonia.Controls
if (allowVertScrollbar)
{
if (cellsHeight > 0 &&
- DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
+ MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{
cellsWidth -= vertScrollBarWidth;
@@ -3439,9 +3439,9 @@ namespace Avalonia.Controls
if (allowHorizScrollbar)
{
if (cellsWidth > 0 &&
- DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight) &&
- DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) &&
- DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth))
+ MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight) &&
+ MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
+ MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth))
{
cellsHeight -= horizScrollBarHeight;
Debug.Assert(cellsHeight >= 0);
@@ -5387,7 +5387,7 @@ namespace Avalonia.Controls
private void SetVerticalOffset(double newVerticalOffset)
{
_verticalOffset = newVerticalOffset;
- if (_vScrollBar != null && !DoubleUtil.AreClose(newVerticalOffset, _vScrollBar.Value))
+ if (_vScrollBar != null && !MathUtilities.AreClose(newVerticalOffset, _vScrollBar.Value))
{
_vScrollBar.Value = _verticalOffset;
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
index df2b03798a..e6cc7e5e40 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
@@ -301,7 +301,7 @@ namespace Avalonia.Controls
private static bool CanResizeColumn(DataGridColumn column)
{
if (column.OwningGrid != null && column.OwningGrid.ColumnsInternal != null && column.OwningGrid.UsesStarSizing &&
- (column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !DoubleUtil.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth)))
+ (column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !MathUtilities.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth)))
{
return false;
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
index 16b63ad696..5b75bc73f9 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
@@ -44,7 +44,7 @@ namespace Avalonia.Controls
/// The remaining amount of adjustment.
internal double AdjustColumnWidths(int displayIndex, double amount, bool userInitiated)
{
- if (!DoubleUtil.IsZero(amount))
+ if (!MathUtilities.IsZero(amount))
{
if (amount < 0)
{
@@ -777,7 +777,7 @@ namespace Avalonia.Controls
private double AdjustStarColumnWidths(int displayIndex, double adjustment, bool userInitiated)
{
double remainingAdjustment = adjustment;
- if (DoubleUtil.IsZero(remainingAdjustment))
+ if (MathUtilities.IsZero(remainingAdjustment))
{
return remainingAdjustment;
}
@@ -843,7 +843,7 @@ namespace Avalonia.Controls
/// The remaining amount of adjustment.
private double AdjustStarColumnWidths(int displayIndex, double remainingAdjustment, bool userInitiated, Func targetWidth)
{
- if (DoubleUtil.IsZero(remainingAdjustment))
+ if (MathUtilities.IsZero(remainingAdjustment))
{
return remainingAdjustment;
}
@@ -1244,7 +1244,7 @@ namespace Avalonia.Controls
Debug.Assert(amount < 0);
Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star);
- if (DoubleUtil.GreaterThanOrClose(targetWidth, column.Width.DisplayValue))
+ if (MathUtilities.GreaterThanOrClose(targetWidth, column.Width.DisplayValue))
{
return amount;
}
@@ -1271,7 +1271,7 @@ namespace Avalonia.Controls
/// The remaining amount of adjustment.
private double DecreaseNonStarColumnWidths(int displayIndex, Func targetWidth, double amount, bool reverse, bool affectNewColumns)
{
- if (DoubleUtil.GreaterThanOrClose(amount, 0))
+ if (MathUtilities.GreaterThanOrClose(amount, 0))
{
return amount;
}
@@ -1285,7 +1285,7 @@ namespace Avalonia.Controls
(affectNewColumns || column.IsInitialDesiredWidthDetermined)))
{
amount = DecreaseNonStarColumnWidth(column, Math.Max(column.ActualMinWidth, targetWidth(column)), amount);
- if (DoubleUtil.IsZero(amount))
+ if (MathUtilities.IsZero(amount))
{
break;
}
@@ -1392,7 +1392,7 @@ namespace Avalonia.Controls
/// The remaining amount of adjustment.
private double IncreaseNonStarColumnWidths(int displayIndex, Func targetWidth, double amount, bool reverse, bool affectNewColumns)
{
- if (DoubleUtil.LessThanOrClose(amount, 0))
+ if (MathUtilities.LessThanOrClose(amount, 0))
{
return amount;
}
@@ -1406,7 +1406,7 @@ namespace Avalonia.Controls
(affectNewColumns || column.IsInitialDesiredWidthDetermined)))
{
amount = IncreaseNonStarColumnWidth(column, Math.Min(column.ActualMaxWidth, targetWidth(column)), amount);
- if (DoubleUtil.IsZero(amount))
+ if (MathUtilities.IsZero(amount))
{
break;
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridLength.cs b/src/Avalonia.Controls.DataGrid/DataGridLength.cs
index 6a545a35ec..4841ddd494 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridLength.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridLength.cs
@@ -529,7 +529,7 @@ namespace Avalonia.Controls
// in this case drop value part and print only "Star"
case DataGridLengthUnitType.Star:
return (
- DoubleUtil.AreClose(1.0, dataGridLength.Value.Value)
+ MathUtilities.AreClose(1.0, dataGridLength.Value.Value)
? _starSuffix
: Convert.ToString(dataGridLength.Value.Value, culture ?? CultureInfo.CurrentCulture) + DataGridLengthConverter._starSuffix);
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
index df200240ff..830eff1102 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
@@ -879,7 +879,7 @@ namespace Avalonia.Controls
&& (double.IsNaN(_detailsContent.Height))
&& (AreDetailsVisible)
&& (!double.IsNaN(_detailsDesiredHeight))
- && !DoubleUtil.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight)
+ && !MathUtilities.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight)
&& Slot != -1)
{
_detailsDesiredHeight = _detailsContent.Bounds.Inflate(_detailsContent.Margin).Height;
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs
index fda14cf5b4..924156f5f4 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs
@@ -329,7 +329,7 @@ namespace Avalonia.Controls
internal void OnRowsMeasure()
{
- if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight))
+ if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight))
{
ScrollSlotsByHeight(DisplayData.PendingVerticalScrollHeight);
DisplayData.PendingVerticalScrollHeight = 0;
@@ -432,7 +432,7 @@ namespace Avalonia.Controls
}
else if (DisplayData.FirstScrollingSlot == slot && slot != -1)
{
- if (!DoubleUtil.IsZero(NegVerticalOffset))
+ if (!MathUtilities.IsZero(NegVerticalOffset))
{
// First displayed row is partially scrolled of. Let's scroll it so that NegVerticalOffset becomes 0.
DisplayData.PendingVerticalScrollHeight = -NegVerticalOffset;
@@ -447,7 +447,7 @@ namespace Avalonia.Controls
{
// Scroll up to the new row so it becomes the first displayed row
firstFullSlot = DisplayData.FirstScrollingSlot - 1;
- if (DoubleUtil.GreaterThan(NegVerticalOffset, 0))
+ if (MathUtilities.GreaterThan(NegVerticalOffset, 0))
{
deltaY = -NegVerticalOffset;
}
@@ -470,7 +470,7 @@ namespace Avalonia.Controls
// Figure out how much of the last row is cut off
double rowHeight = GetExactSlotElementHeight(DisplayData.LastScrollingSlot);
double availableHeight = AvailableSlotElementRoom + rowHeight;
- if (DoubleUtil.AreClose(rowHeight, availableHeight))
+ if (MathUtilities.AreClose(rowHeight, availableHeight))
{
if (DisplayData.LastScrollingSlot == slot)
{
@@ -499,7 +499,7 @@ namespace Avalonia.Controls
{
ResetDisplayedRows();
}
- if (DoubleUtil.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight))
+ if (MathUtilities.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight))
{
// The entire row won't fit in the DataGrid so we start showing it from the top
NegVerticalOffset = 0;
@@ -519,7 +519,7 @@ namespace Avalonia.Controls
}
//
- Debug.Assert(DoubleUtil.LessThanOrClose(NegVerticalOffset, _verticalOffset));
+ Debug.Assert(MathUtilities.LessThanOrClose(NegVerticalOffset, _verticalOffset));
SetVerticalOffset(_verticalOffset);
@@ -1660,7 +1660,7 @@ namespace Avalonia.Controls
private void ScrollSlotsByHeight(double height)
{
Debug.Assert(DisplayData.FirstScrollingSlot >= 0);
- Debug.Assert(!DoubleUtil.IsZero(height));
+ Debug.Assert(!MathUtilities.IsZero(height));
_scrollingByHeight = true;
try
@@ -1672,7 +1672,7 @@ namespace Avalonia.Controls
{
// Scrolling Down
int lastVisibleSlot = GetPreviousVisibleSlot(SlotCount);
- if (_vScrollBar != null && DoubleUtil.AreClose(_vScrollBar.Maximum, newVerticalOffset))
+ if (_vScrollBar != null && MathUtilities.AreClose(_vScrollBar.Maximum, newVerticalOffset))
{
// We've scrolled to the bottom of the ScrollBar, automatically place the user at the very bottom
// of the DataGrid. If this produces very odd behavior, evaluate the coping strategy used by
@@ -1684,7 +1684,7 @@ namespace Avalonia.Controls
else
{
deltaY = GetSlotElementHeight(newFirstScrollingSlot) - NegVerticalOffset;
- if (DoubleUtil.LessThan(height, deltaY))
+ if (MathUtilities.LessThan(height, deltaY))
{
// We've merely covered up more of the same row we're on
NegVerticalOffset += height;
@@ -1707,7 +1707,7 @@ namespace Avalonia.Controls
}
else
{
- while (DoubleUtil.LessThanOrClose(deltaY, height))
+ while (MathUtilities.LessThanOrClose(deltaY, height))
{
if (newFirstScrollingSlot < lastVisibleSlot)
{
@@ -1727,7 +1727,7 @@ namespace Avalonia.Controls
double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot);
double remainingHeight = height - deltaY;
- if (DoubleUtil.LessThanOrClose(rowHeight, remainingHeight))
+ if (MathUtilities.LessThanOrClose(rowHeight, remainingHeight))
{
deltaY += rowHeight;
}
@@ -1744,7 +1744,7 @@ namespace Avalonia.Controls
else
{
// Scrolling Up
- if (DoubleUtil.GreaterThanOrClose(height + NegVerticalOffset, 0))
+ if (MathUtilities.GreaterThanOrClose(height + NegVerticalOffset, 0))
{
// We've merely exposing more of the row we're on
NegVerticalOffset += height;
@@ -1778,7 +1778,7 @@ namespace Avalonia.Controls
else
{
int lastScrollingSlot = DisplayData.LastScrollingSlot;
- while (DoubleUtil.GreaterThan(deltaY, height))
+ while (MathUtilities.GreaterThan(deltaY, height))
{
if (newFirstScrollingSlot > 0)
{
@@ -1797,7 +1797,7 @@ namespace Avalonia.Controls
}
double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot);
double remainingHeight = height - deltaY;
- if (DoubleUtil.LessThanOrClose(rowHeight + remainingHeight, 0))
+ if (MathUtilities.LessThanOrClose(rowHeight + remainingHeight, 0))
{
deltaY -= rowHeight;
}
@@ -1809,7 +1809,7 @@ namespace Avalonia.Controls
}
}
}
- if (DoubleUtil.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0)
+ if (MathUtilities.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0)
{
// We've scrolled to the top of the ScrollBar, automatically place the user at the very top
// of the DataGrid. If this produces very odd behavior, evaluate the RowHeight estimate.
@@ -1822,7 +1822,7 @@ namespace Avalonia.Controls
}
double firstRowHeight = GetExactSlotElementHeight(newFirstScrollingSlot);
- if (DoubleUtil.LessThan(firstRowHeight, NegVerticalOffset))
+ if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset))
{
// We've scrolled off more of the first row than what's possible. This can happen
// if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling
@@ -1838,11 +1838,11 @@ namespace Avalonia.Controls
UpdateDisplayedRows(newFirstScrollingSlot, CellsHeight);
double firstElementHeight = GetExactSlotElementHeight(DisplayData.FirstScrollingSlot);
- if (DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight))
+ if (MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight))
{
int firstElementSlot = DisplayData.FirstScrollingSlot;
// We filled in some rows at the top and now we have a NegVerticalOffset that's greater than the first element
- while (newFirstScrollingSlot > 0 && DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight))
+ while (newFirstScrollingSlot > 0 && MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight))
{
int previousSlot = GetPreviousVisibleSlot(firstElementSlot);
if (previousSlot == -1)
@@ -1872,7 +1872,7 @@ namespace Avalonia.Controls
{
_verticalOffset = NegVerticalOffset;
}
- else if (DoubleUtil.GreaterThan(NegVerticalOffset, newVerticalOffset))
+ else if (MathUtilities.GreaterThan(NegVerticalOffset, newVerticalOffset))
{
// The scrolled-in row was larger than anticipated. Adjust the DataGrid so the ScrollBar thumb
// can stay in the same place
@@ -1890,8 +1890,8 @@ namespace Avalonia.Controls
DisplayData.FullyRecycleElements();
- Debug.Assert(DoubleUtil.GreaterThanOrClose(NegVerticalOffset, 0));
- Debug.Assert(DoubleUtil.GreaterThanOrClose(_verticalOffset, NegVerticalOffset));
+ Debug.Assert(MathUtilities.GreaterThanOrClose(NegVerticalOffset, 0));
+ Debug.Assert(MathUtilities.GreaterThanOrClose(_verticalOffset, NegVerticalOffset));
}
finally
{
@@ -2032,7 +2032,7 @@ namespace Avalonia.Controls
double deltaY = -NegVerticalOffset;
int visibleScrollingRows = 0;
- if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0)
+ if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0)
{
return;
}
@@ -2044,7 +2044,7 @@ namespace Avalonia.Controls
}
int slot = firstDisplayedScrollingSlot;
- while (slot < SlotCount && !DoubleUtil.GreaterThanOrClose(deltaY, displayHeight))
+ while (slot < SlotCount && !MathUtilities.GreaterThanOrClose(deltaY, displayHeight))
{
deltaY += GetExactSlotElementHeight(slot);
visibleScrollingRows++;
@@ -2052,7 +2052,7 @@ namespace Avalonia.Controls
slot = GetNextVisibleSlot(slot);
}
- while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0)
+ while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0)
{
slot = GetPreviousVisibleSlot(firstDisplayedScrollingSlot);
if (slot >= 0)
@@ -2063,14 +2063,14 @@ namespace Avalonia.Controls
}
}
// If we're up to the first row, and we still have room left, uncover as much of the first row as we can
- if (firstDisplayedScrollingSlot == 0 && DoubleUtil.LessThan(deltaY, displayHeight))
+ if (firstDisplayedScrollingSlot == 0 && MathUtilities.LessThan(deltaY, displayHeight))
{
double newNegVerticalOffset = Math.Max(0, NegVerticalOffset - displayHeight + deltaY);
deltaY += NegVerticalOffset - newNegVerticalOffset;
NegVerticalOffset = newNegVerticalOffset;
}
- if (DoubleUtil.GreaterThan(deltaY, displayHeight) || (DoubleUtil.AreClose(deltaY, displayHeight) && DoubleUtil.GreaterThan(NegVerticalOffset, 0)))
+ if (MathUtilities.GreaterThan(deltaY, displayHeight) || (MathUtilities.AreClose(deltaY, displayHeight) && MathUtilities.GreaterThan(NegVerticalOffset, 0)))
{
DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows - 1;
}
@@ -2108,7 +2108,7 @@ namespace Avalonia.Controls
double deltaY = 0;
int visibleScrollingRows = 0;
- if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0)
+ if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0)
{
ResetDisplayedRows();
return;
@@ -2120,7 +2120,7 @@ namespace Avalonia.Controls
}
int slot = lastDisplayedScrollingRow;
- while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0)
+ while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0)
{
deltaY += GetExactSlotElementHeight(slot);
visibleScrollingRows++;
@@ -2542,7 +2542,7 @@ namespace Avalonia.Controls
double heightChange = UpdateRowGroupVisibility(rowGroupInfo, isVisible, isDisplayed: false);
// Use epsilon instead of 0 here so that in the off chance that our estimates put the vertical offset negative
// the user can still scroll to the top since the offset is non-zero
- SetVerticalOffset(Math.Max(DoubleUtil.DBL_EPSILON, _verticalOffset + heightChange));
+ SetVerticalOffset(Math.Max(MathUtilities.DoubleEpsilon, _verticalOffset + heightChange));
}
else
{
@@ -3024,4 +3024,4 @@ namespace Avalonia.Controls
}
#endif
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
index 0f513e7f42..6e0703c90f 100644
--- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
+++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
@@ -310,9 +310,9 @@ namespace Avalonia.Controls.Primitives
double leftEdge = column.IsFrozen ? frozenLeftEdge : scrollingLeftEdge;
double rightEdge = leftEdge + column.ActualWidth;
return
- DoubleUtil.GreaterThan(rightEdge, 0) &&
- DoubleUtil.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) &&
- DoubleUtil.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s)
+ MathUtilities.GreaterThan(rightEdge, 0) &&
+ MathUtilities.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) &&
+ MathUtilities.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s)
}
}
}
diff --git a/src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs b/src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs
deleted file mode 100644
index ec0e8836d7..0000000000
--- a/src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-// (c) Copyright Microsoft Corporation.
-// This source is subject to the Microsoft Public License (Ms-PL).
-// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
-// All other rights reserved.
-
-using System;
-
-namespace Avalonia.Controls.Utils
-{
- internal static class DoubleUtil
- {
- internal const double DBL_EPSILON = 1e-6;
-
- ///
- /// AreClose - Returns whether or not two doubles are "close". That is, whether or
- /// not they are within epsilon of each other. Note that this epsilon is proportional
- /// to the numbers themselves to that AreClose survives scalar multiplication.
- /// There are plenty of ways for this to return false even for numbers which
- /// are theoretically identical, so no code calling this should fail to work if this
- /// returns false. This is important enough to repeat:
- /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
- /// used for optimizations *only*.
- ///
- ///
- /// bool - the result of the AreClose comparison.
- ///
- /// The first double to compare.
- /// The second double to compare.
- public static bool AreClose(double value1, double value2)
- {
- //in case they are Infinities (then epsilon check does not work)
- if (value1 == value2) return true;
- // This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DBL_EPSILON
- double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DBL_EPSILON;
- double delta = value1 - value2;
- return (-eps < delta) && (eps > delta);
- }
-
- ///
- /// GreaterThan - Returns whether or not the first double is greater than the second double.
- /// That is, whether or not the first is strictly greater than *and* not within epsilon of
- /// the other number. Note that this epsilon is proportional to the numbers themselves
- /// to that AreClose survives scalar multiplication. Note,
- /// There are plenty of ways for this to return false even for numbers which
- /// are theoretically identical, so no code calling this should fail to work if this
- /// returns false. This is important enough to repeat:
- /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
- /// used for optimizations *only*.
- ///
- ///
- /// bool - the result of the GreaterThan comparison.
- ///
- /// The first double to compare.
- /// The second double to compare.
- public static bool GreaterThan(double value1, double value2)
- {
- return (value1 > value2) && !AreClose(value1, value2);
- }
-
- ///
- /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to
- /// the second double. That is, whether or not the first is strictly greater than or within
- /// epsilon of the other number. Note that this epsilon is proportional to the numbers
- /// themselves to that AreClose survives scalar multiplication. Note,
- /// There are plenty of ways for this to return false even for numbers which
- /// are theoretically identical, so no code calling this should fail to work if this
- /// returns false. This is important enough to repeat:
- /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
- /// used for optimizations *only*.
- ///
- ///
- /// bool - the result of the GreaterThanOrClose comparison.
- ///
- /// The first double to compare.
- /// The second double to compare.
- public static bool GreaterThanOrClose(double value1, double value2)
- {
- return (value1 > value2) || AreClose(value1, value2);
- }
-
- ///
- /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0),
- /// but this is faster.
- ///
- ///
- /// bool - the result of the IsZero comparison.
- ///
- /// The double to compare to 0.
- public static bool IsZero(double value)
- {
- return Math.Abs(value) < 10.0 * DBL_EPSILON;
- }
-
- ///
- /// LessThan - Returns whether or not the first double is less than the second double.
- /// That is, whether or not the first is strictly less than *and* not within epsilon of
- /// the other number. Note that this epsilon is proportional to the numbers themselves
- /// to that AreClose survives scalar multiplication. Note,
- /// There are plenty of ways for this to return false even for numbers which
- /// are theoretically identical, so no code calling this should fail to work if this
- /// returns false. This is important enough to repeat:
- /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
- /// used for optimizations *only*.
- ///
- ///
- /// bool - the result of the LessThan comparison.
- ///
- /// The first double to compare.
- /// The second double to compare.
- public static bool LessThan(double value1, double value2)
- {
- return (value1 < value2) && !AreClose(value1, value2);
- }
-
- ///
- /// LessThanOrClose - Returns whether or not the first double is less than or close to
- /// the second double. That is, whether or not the first is strictly less than or within
- /// epsilon of the other number. Note that this epsilon is proportional to the numbers
- /// themselves to that AreClose survives scalar multiplication. Note,
- /// There are plenty of ways for this to return false even for numbers which
- /// are theoretically identical, so no code calling this should fail to work if this
- /// returns false. This is important enough to repeat:
- /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
- /// used for optimizations *only*.
- ///
- ///
- /// bool - the result of the LessThanOrClose comparison.
- ///
- /// The first double to compare.
- /// The second double to compare.
- public static bool LessThanOrClose(double value1, double value2)
- {
- return (value1 < value2) || AreClose(value1, value2);
- }
- }
-}
diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs
index b38cc56a17..31101dc0f1 100644
--- a/src/Avalonia.Controls/AutoCompleteBox.cs
+++ b/src/Avalonia.Controls/AutoCompleteBox.cs
@@ -468,10 +468,11 @@ namespace Avalonia.Controls
///
/// dependency property.
public static readonly DirectProperty TextProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(Text),
+ TextBlock.TextProperty.AddOwnerWithDataValidation(
o => o.Text,
- (o, v) => o.Text = v);
+ (o, v) => o.Text = v,
+ defaultBindingMode: BindingMode.TwoWay,
+ enableDataValidation: true);
///
/// Identifies the
@@ -1244,6 +1245,20 @@ namespace Avalonia.Controls
base.OnApplyTemplate(e);
}
+
+ ///
+ /// Called to update the validation state for properties for which data validation is
+ /// enabled.
+ ///
+ /// The property.
+ /// The new binding value for the property.
+ protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value)
+ {
+ if (property == TextProperty)
+ {
+ DataValidationErrors.SetError(this, value.Error);
+ }
+ }
///
/// Provides handling for the
diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs
index 53c6a54b4d..4cf7db74d9 100644
--- a/src/Avalonia.Controls/Calendar/Calendar.cs
+++ b/src/Avalonia.Controls/Calendar/Calendar.cs
@@ -998,10 +998,10 @@ namespace Avalonia.Controls
///
- /// Gets or sets a value indicating whether DatePicker should change its
+ /// Gets or sets a value indicating whether CalendarDatePicker should change its
/// DisplayDate because of a SelectedDate change on its Calendar.
///
- internal bool DatePickerDisplayDateFlag { get; set; }
+ internal bool CalendarDatePickerDisplayDateFlag { get; set; }
internal CalendarDayButton FindDayButtonFromDay(DateTime day)
{
diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
similarity index 87%
rename from src/Avalonia.Controls/Calendar/DatePicker.cs
rename to src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
index 0f53dc1364..b987f065be 100644
--- a/src/Avalonia.Controls/Calendar/DatePicker.cs
+++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
@@ -16,29 +16,29 @@ namespace Avalonia.Controls
{
///
/// Provides data for the
- ///
+ ///
/// event.
///
- public class DatePickerDateValidationErrorEventArgs : EventArgs
+ public class CalendarDatePickerDateValidationErrorEventArgs : EventArgs
{
private bool _throwException;
///
/// Initializes a new instance of the
- ///
+ ///
/// class.
///
///
/// The initial exception from the
- ///
+ ///
/// event.
///
///
/// The text that caused the
- ///
+ ///
/// event.
///
- public DatePickerDateValidationErrorEventArgs(Exception exception, string text)
+ public CalendarDatePickerDateValidationErrorEventArgs(Exception exception, string text)
{
this.Text = text;
this.Exception = exception;
@@ -46,7 +46,7 @@ namespace Avalonia.Controls
///
/// Gets the initial exception associated with the
- ///
+ ///
/// event.
///
///
@@ -56,7 +56,7 @@ namespace Avalonia.Controls
///
/// Gets the text that caused the
- ///
+ ///
/// event.
///
///
@@ -66,7 +66,7 @@ namespace Avalonia.Controls
///
/// Gets or sets a value indicating whether
- ///
+ ///
/// should be thrown.
///
///
@@ -74,7 +74,7 @@ namespace Avalonia.Controls
///
///
/// If set to true and
- ///
+ ///
/// is null.
///
public bool ThrowException
@@ -93,9 +93,9 @@ namespace Avalonia.Controls
///
/// Specifies date formats for a
- /// .
+ /// .
///
- public enum DatePickerFormat
+ public enum CalendarDatePickerFormat
{
///
/// Specifies that the date should be displayed using unabbreviated days
@@ -115,7 +115,7 @@ namespace Avalonia.Controls
Custom = 2
}
- public class DatePicker : TemplatedControl
+ public class CalendarDatePicker : TemplatedControl
{
private const string ElementTextBox = "PART_TextBox";
private const string ElementButton = "PART_Button";
@@ -154,59 +154,59 @@ namespace Avalonia.Controls
///
public CalendarBlackoutDatesCollection BlackoutDates { get; private set; }
- public static readonly DirectProperty DisplayDateProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty DisplayDateProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(DisplayDate),
o => o.DisplayDate,
(o, v) => o.DisplayDate = v);
- public static readonly DirectProperty DisplayDateStartProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty DisplayDateStartProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(DisplayDateStart),
o => o.DisplayDateStart,
(o, v) => o.DisplayDateStart = v);
- public static readonly DirectProperty DisplayDateEndProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty DisplayDateEndProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(DisplayDateEnd),
o => o.DisplayDateEnd,
(o, v) => o.DisplayDateEnd = v);
public static readonly StyledProperty FirstDayOfWeekProperty =
- AvaloniaProperty.Register(nameof(FirstDayOfWeek));
+ AvaloniaProperty.Register(nameof(FirstDayOfWeek));
- public static readonly DirectProperty IsDropDownOpenProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty IsDropDownOpenProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(IsDropDownOpen),
o => o.IsDropDownOpen,
(o, v) => o.IsDropDownOpen = v);
public static readonly StyledProperty IsTodayHighlightedProperty =
- AvaloniaProperty.Register(nameof(IsTodayHighlighted));
- public static readonly DirectProperty SelectedDateProperty =
- AvaloniaProperty.RegisterDirect(
+ AvaloniaProperty.Register(nameof(IsTodayHighlighted));
+ public static readonly DirectProperty SelectedDateProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(SelectedDate),
o => o.SelectedDate,
(o, v) => o.SelectedDate = v);
- public static readonly StyledProperty SelectedDateFormatProperty =
- AvaloniaProperty.Register(
+ public static readonly StyledProperty SelectedDateFormatProperty =
+ AvaloniaProperty.Register(
nameof(SelectedDateFormat),
- defaultValue: DatePickerFormat.Short,
+ defaultValue: CalendarDatePickerFormat.Short,
validate: IsValidSelectedDateFormat);
public static readonly StyledProperty CustomDateFormatStringProperty =
- AvaloniaProperty.Register(
+ AvaloniaProperty.Register(
nameof(CustomDateFormatString),
defaultValue: "d",
validate: IsValidDateFormatString);
- public static readonly DirectProperty TextProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty TextProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(Text),
o => o.Text,
(o, v) => o.Text = v);
public static readonly StyledProperty WatermarkProperty =
- TextBox.WatermarkProperty.AddOwner();
+ TextBox.WatermarkProperty.AddOwner();
public static readonly StyledProperty UseFloatingWatermarkProperty =
- TextBox.UseFloatingWatermarkProperty.AddOwner();
+ TextBox.UseFloatingWatermarkProperty.AddOwner();
///
@@ -218,9 +218,9 @@ namespace Avalonia.Controls
///
///
/// The specified date is not in the range defined by
- ///
+ ///
/// and
- /// .
+ /// .
///
public DateTime DisplayDate
{
@@ -320,7 +320,7 @@ namespace Avalonia.Controls
///
/// An specified format is not valid.
///
- public DatePickerFormat SelectedDateFormat
+ public CalendarDatePickerFormat SelectedDateFormat
{
get { return GetValue(SelectedDateFormatProperty); }
set { SetValue(SelectedDateFormatProperty, value); }
@@ -380,33 +380,33 @@ namespace Avalonia.Controls
/// Occurs when
/// is assigned a value that cannot be interpreted as a date.
///
- public event EventHandler DateValidationError;
+ public event EventHandler DateValidationError;
///
/// Occurs when the
- ///
+ ///
/// property is changed.
///
public event EventHandler SelectedDateChanged;
- static DatePicker()
+ static CalendarDatePicker()
{
- FocusableProperty.OverrideDefaultValue(true);
-
- DisplayDateProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateChanged(e));
- DisplayDateStartProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateStartChanged(e));
- DisplayDateEndProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateEndChanged(e));
- IsDropDownOpenProperty.Changed.AddClassHandler((x,e) => x.OnIsDropDownOpenChanged(e));
- SelectedDateProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateChanged(e));
- SelectedDateFormatProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateFormatChanged(e));
- CustomDateFormatStringProperty.Changed.AddClassHandler((x,e) => x.OnCustomDateFormatStringChanged(e));
- TextProperty.Changed.AddClassHandler((x,e) => x.OnTextChanged(e));
+ FocusableProperty.OverrideDefaultValue(true);
+
+ DisplayDateProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateChanged(e));
+ DisplayDateStartProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateStartChanged(e));
+ DisplayDateEndProperty.Changed.AddClassHandler((x,e) => x.OnDisplayDateEndChanged(e));
+ IsDropDownOpenProperty.Changed.AddClassHandler((x,e) => x.OnIsDropDownOpenChanged(e));
+ SelectedDateProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateChanged(e));
+ SelectedDateFormatProperty.Changed.AddClassHandler((x,e) => x.OnSelectedDateFormatChanged(e));
+ CustomDateFormatStringProperty.Changed.AddClassHandler((x,e) => x.OnCustomDateFormatStringChanged(e));
+ TextProperty.Changed.AddClassHandler((x,e) => x.OnTextChanged(e));
}
///
/// Initializes a new instance of the
/// class.
///
- public DatePicker()
+ public CalendarDatePicker()
{
FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek;
_defaultText = string.Empty;
@@ -662,12 +662,12 @@ namespace Avalonia.Controls
// change is coming from the Calendar UI itself, so, we
// shouldn't change the DisplayDate since it will automatically
// be changed by the Calendar
- if ((day.Month != DisplayDate.Month || day.Year != DisplayDate.Year) && (_calendar == null || !_calendar.DatePickerDisplayDateFlag))
+ if ((day.Month != DisplayDate.Month || day.Year != DisplayDate.Year) && (_calendar == null || !_calendar.CalendarDatePickerDisplayDateFlag))
{
DisplayDate = day;
}
if(_calendar != null)
- _calendar.DatePickerDisplayDateFlag = false;
+ _calendar.CalendarDatePickerDisplayDateFlag = false;
}
else
{
@@ -707,7 +707,7 @@ namespace Avalonia.Controls
}
private void OnCustomDateFormatStringChanged(AvaloniaPropertyChangedEventArgs e)
{
- if(SelectedDateFormat == DatePickerFormat.Custom)
+ if(SelectedDateFormat == CalendarDatePickerFormat.Custom)
{
OnDateFormatChanged();
}
@@ -752,15 +752,15 @@ namespace Avalonia.Controls
///
/// Raises the
- ///
+ ///
/// event.
///
///
/// A
- ///
+ ///
/// that contains the event data.
///
- protected virtual void OnDateValidationError(DatePickerDateValidationErrorEventArgs e)
+ protected virtual void OnDateValidationError(CalendarDatePickerDateValidationErrorEventArgs e)
{
DateValidationError?.Invoke(this, e);
}
@@ -959,7 +959,7 @@ namespace Avalonia.Controls
}
else
{
- var dateValidationError = new DatePickerDateValidationErrorEventArgs(new ArgumentOutOfRangeException(nameof(text), "SelectedDate value is not valid."), text);
+ var dateValidationError = new CalendarDatePickerDateValidationErrorEventArgs(new ArgumentOutOfRangeException(nameof(text), "SelectedDate value is not valid."), text);
OnDateValidationError(dateValidationError);
if (dateValidationError.ThrowException)
@@ -970,7 +970,7 @@ namespace Avalonia.Controls
}
catch (FormatException ex)
{
- DatePickerDateValidationErrorEventArgs textParseError = new DatePickerDateValidationErrorEventArgs(ex, text);
+ CalendarDatePickerDateValidationErrorEventArgs textParseError = new CalendarDatePickerDateValidationErrorEventArgs(ex, text);
OnDateValidationError(textParseError);
if (textParseError.ThrowException)
@@ -986,11 +986,11 @@ namespace Avalonia.Controls
switch (SelectedDateFormat)
{
- case DatePickerFormat.Short:
+ case CalendarDatePickerFormat.Short:
return string.Format(CultureInfo.CurrentCulture, d.ToString(dtfi.ShortDatePattern, dtfi));
- case DatePickerFormat.Long:
+ case CalendarDatePickerFormat.Long:
return string.Format(CultureInfo.CurrentCulture, d.ToString(dtfi.LongDatePattern, dtfi));
- case DatePickerFormat.Custom:
+ case CalendarDatePickerFormat.Custom:
return string.Format(CultureInfo.CurrentCulture, d.ToString(CustomDateFormatString, dtfi));
}
return null;
@@ -1118,12 +1118,12 @@ namespace Avalonia.Controls
switch (SelectedDateFormat)
{
- case DatePickerFormat.Long:
+ case CalendarDatePickerFormat.Long:
{
watermarkText = string.Format(CultureInfo.CurrentCulture, watermarkFormat, dtfi.LongDatePattern.ToString());
break;
}
- case DatePickerFormat.Short:
+ case CalendarDatePickerFormat.Short:
default:
{
watermarkText = string.Format(CultureInfo.CurrentCulture, watermarkFormat, dtfi.ShortDatePattern.ToString());
@@ -1139,11 +1139,11 @@ namespace Avalonia.Controls
}
}
- private static bool IsValidSelectedDateFormat(DatePickerFormat value)
+ private static bool IsValidSelectedDateFormat(CalendarDatePickerFormat value)
{
- return value == DatePickerFormat.Long
- || value == DatePickerFormat.Short
- || value == DatePickerFormat.Custom;
+ return value == CalendarDatePickerFormat.Long
+ || value == CalendarDatePickerFormat.Short
+ || value == CalendarDatePickerFormat.Custom;
}
private static bool IsValidDateFormatString(string formatString)
{
diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs
index ece0ef97d9..0be7c4f67e 100644
--- a/src/Avalonia.Controls/Calendar/CalendarItem.cs
+++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs
@@ -909,7 +909,7 @@ namespace Avalonia.Controls.Primitives
case CalendarSelectionMode.SingleDate:
{
DateTime selectedDate = (DateTime)b.DataContext;
- Owner.DatePickerDisplayDateFlag = true;
+ Owner.CalendarDatePickerDisplayDateFlag = true;
if (Owner.SelectedDates.Count == 0)
{
Owner.SelectedDates.Add(selectedDate);
@@ -981,7 +981,7 @@ namespace Avalonia.Controls.Primitives
}
case CalendarSelectionMode.SingleDate:
{
- Owner.DatePickerDisplayDateFlag = true;
+ Owner.CalendarDatePickerDisplayDateFlag = true;
if (Owner.SelectedDates.Count == 0)
{
Owner.SelectedDates.Add(selectedDate);
diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index 86499530da..5929dd39d4 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -1,17 +1,18 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
-using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
-using Avalonia.LogicalTree;
using Avalonia.Styling;
+#nullable enable
+
namespace Avalonia.Controls
{
///
@@ -19,11 +20,59 @@ namespace Avalonia.Controls
///
public class ContextMenu : MenuBase, ISetterValue
{
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty HorizontalOffsetProperty =
+ Popup.HorizontalOffsetProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty VerticalOffsetProperty =
+ Popup.VerticalOffsetProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementAnchorProperty =
+ Popup.PlacementAnchorProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementConstraintAdjustmentProperty =
+ Popup.PlacementConstraintAdjustmentProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementGravityProperty =
+ Popup.PlacementGravityProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementModeProperty =
+ Popup.PlacementModeProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementRectProperty =
+ AvaloniaProperty.Register(nameof(PlacementRect));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementTargetProperty =
+ Popup.PlacementTargetProperty.AddOwner();
+
private static readonly ITemplate DefaultPanel =
new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical });
- private Popup _popup;
- private List _attachedControls;
- private IInputElement _previousFocus;
+ private Popup? _popup;
+ private List? _attachedControls;
+ private IInputElement? _previousFocus;
///
/// Initializes a new instance of the class.
@@ -47,23 +96,107 @@ namespace Avalonia.Controls
///
static ContextMenu()
{
- ItemsPanelProperty.OverrideDefaultValue(typeof(ContextMenu), DefaultPanel);
+ ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
+ PlacementModeProperty.OverrideDefaultValue(PlacementMode.Pointer);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
}
+ ///
+ /// Gets or sets the Horizontal offset of the context menu in relation to the .
+ ///
+ public double HorizontalOffset
+ {
+ get { return GetValue(HorizontalOffsetProperty); }
+ set { SetValue(HorizontalOffsetProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the Vertical offset of the context menu in relation to the .
+ ///
+ public double VerticalOffset
+ {
+ get { return GetValue(VerticalOffsetProperty); }
+ set { SetValue(VerticalOffsetProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the anchor point on the when
+ /// is .
+ ///
+ public PopupAnchor PlacementAnchor
+ {
+ get { return GetValue(PlacementAnchorProperty); }
+ set { SetValue(PlacementAnchorProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value describing how the context menu position will be adjusted if the
+ /// unadjusted position would result in the context menu being partly constrained.
+ ///
+ public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
+ {
+ get { return GetValue(PlacementConstraintAdjustmentProperty); }
+ set { SetValue(PlacementConstraintAdjustmentProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value which defines in what direction the context menu should open
+ /// when is .
+ ///
+ public PopupGravity PlacementGravity
+ {
+ get { return GetValue(PlacementGravityProperty); }
+ set { SetValue(PlacementGravityProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the placement mode of the context menu in relation to the .
+ ///
+ public PlacementMode PlacementMode
+ {
+ get { return GetValue(PlacementModeProperty); }
+ set { SetValue(PlacementModeProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the the anchor rectangle within the parent that the context menu will be placed
+ /// relative to when is .
+ ///
+ ///
+ /// The placement rect defines a rectangle relative to around
+ /// which the popup will be opened, with determining which edge
+ /// of the placement target is used.
+ ///
+ /// If unset, the anchor rectangle will be the bounds of the .
+ ///
+ public Rect? PlacementRect
+ {
+ get { return GetValue(PlacementRectProperty); }
+ set { SetValue(PlacementRectProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the control that is used to determine the popup's position.
+ ///
+ public Control? PlacementTarget
+ {
+ get { return GetValue(PlacementTargetProperty); }
+ set { SetValue(PlacementTargetProperty, value); }
+ }
+
///
/// Occurs when the value of the
///
/// property is changing from false to true.
///
- public event CancelEventHandler ContextMenuOpening;
+ public event CancelEventHandler? ContextMenuOpening;
///
/// Occurs when the value of the
///
/// property is changing from true to false.
///
- public event CancelEventHandler ContextMenuClosing;
+ public event CancelEventHandler? ContextMenuClosing;
///
/// Called when the property changes on a control.
@@ -77,7 +210,7 @@ namespace Avalonia.Controls
{
control.PointerReleased -= ControlPointerReleased;
oldMenu._attachedControls?.Remove(control);
- ((ISetLogicalParent)oldMenu._popup)?.SetParent(null);
+ ((ISetLogicalParent?)oldMenu._popup)?.SetParent(null);
}
if (e.NewValue is ContextMenu newMenu)
@@ -97,7 +230,7 @@ namespace Avalonia.Controls
/// Opens a context menu on the specified control.
///
/// The control.
- public void Open(Control control)
+ public void Open(Control? control)
{
if (control is null && (_attachedControls is null || _attachedControls.Count == 0))
{
@@ -113,7 +246,7 @@ namespace Avalonia.Controls
nameof(control));
}
- control ??= _attachedControls[0];
+ control ??= _attachedControls![0];
if (IsOpen)
{
@@ -124,8 +257,14 @@ namespace Avalonia.Controls
{
_popup = new Popup
{
- PlacementMode = PlacementMode.Pointer,
- PlacementTarget = control,
+ HorizontalOffset = HorizontalOffset,
+ VerticalOffset = VerticalOffset,
+ PlacementAnchor = PlacementAnchor,
+ PlacementConstraintAdjustment = PlacementConstraintAdjustment,
+ PlacementGravity = PlacementGravity,
+ PlacementMode = PlacementMode,
+ PlacementRect = PlacementRect,
+ PlacementTarget = PlacementTarget ?? control,
StaysOpen = false
};
@@ -204,7 +343,7 @@ namespace Avalonia.Controls
if (_attachedControls is null || _attachedControls.Count == 0)
{
- ((ISetLogicalParent)_popup).SetParent(null);
+ ((ISetLogicalParent)_popup!).SetParent(null);
}
// HACK: Reset the focus when the popup is closed. We need to fix this so it's automatic.
diff --git a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs
index 54bd6bcf39..9f3a6da9da 100644
--- a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs
+++ b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs
@@ -18,10 +18,24 @@ namespace Avalonia.Controls.Converters
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
- if (!(value is int depth))
- return new Thickness(0);
-
- return new Thickness(Left ? Indent * depth : 0, Top ? Indent * depth : 0, Right ? Indent * depth : 0, Bottom ? Indent * depth : 0);
+ if (value is int scalarDepth)
+ {
+ return new Thickness(
+ Left ? Indent * scalarDepth : 0,
+ Top ? Indent * scalarDepth : 0,
+ Right ? Indent * scalarDepth : 0,
+ Bottom ? Indent * scalarDepth : 0);
+ }
+ else if (value is Thickness thinknessDepth)
+ {
+ return new Thickness(
+ Left ? Indent * thinknessDepth.Left : 0,
+ Top ? Indent * thinknessDepth.Top : 0,
+ Right ? Indent * thinknessDepth.Right : 0,
+ Bottom ? Indent * thinknessDepth.Bottom : 0);
+ }
+ return new Thickness(0);
+
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
diff --git a/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs b/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs
new file mode 100644
index 0000000000..e6420fe342
--- /dev/null
+++ b/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data.Converters;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls.Converters
+{
+ public class MenuScrollingVisibilityConverter : IMultiValueConverter
+ {
+ public static readonly MenuScrollingVisibilityConverter Instance = new MenuScrollingVisibilityConverter();
+
+ public object Convert(IList values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (parameter == null ||
+ values == null ||
+ values.Count != 4 ||
+ !(values[0] is ScrollBarVisibility visiblity) ||
+ !(values[1] is double offset) ||
+ !(values[2] is double extent) ||
+ !(values[3] is double viewport))
+ {
+ return AvaloniaProperty.UnsetValue;
+ }
+
+ if (visiblity == ScrollBarVisibility.Auto)
+ {
+ if (extent == viewport)
+ {
+ return false;
+ }
+
+ double target;
+
+ if (parameter is double d)
+ {
+ target = d;
+ }
+ else if (parameter is string s)
+ {
+ target = double.Parse(s, NumberFormatInfo.InvariantInfo);
+ }
+ else
+ {
+ return AvaloniaProperty.UnsetValue;
+ }
+
+ // Calculate the percent so that we can see if we are near the edge of the range
+ double percent = MathUtilities.Clamp(offset * 100.0 / (extent - viewport), 0, 100);
+
+ if (MathUtilities.AreClose(percent, target))
+ {
+ // We are at the end of the range, so no need for this button to be shown
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs
index 1781067abb..e10d78917e 100644
--- a/src/Avalonia.Controls/Grid.cs
+++ b/src/Avalonia.Controls/Grid.cs
@@ -1228,7 +1228,7 @@ namespace Avalonia.Controls
Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Count);
// avoid processing when asked to distribute "0"
- if (!_IsZero(requestedSize))
+ if (!MathUtilities.IsZero(requestedSize))
{
DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting
int end = start + count;
@@ -1306,7 +1306,7 @@ namespace Avalonia.Controls
}
// sanity check: requested size must all be distributed
- Debug.Assert(_IsZero(sizeToDistribute));
+ Debug.Assert(MathUtilities.IsZero(sizeToDistribute));
}
else if (requestedSize <= rangeMaxSize)
{
@@ -1346,7 +1346,7 @@ namespace Avalonia.Controls
}
// sanity check: requested size must all be distributed
- Debug.Assert(_IsZero(sizeToDistribute));
+ Debug.Assert(MathUtilities.IsZero(sizeToDistribute));
}
else
{
@@ -1358,7 +1358,7 @@ namespace Avalonia.Controls
double equalSize = requestedSize / count;
if (equalSize < maxMaxSize
- && !_AreClose(equalSize, maxMaxSize))
+ && !MathUtilities.AreClose(equalSize, maxMaxSize))
{
// equi-size is less than maximum of maxSizes.
// in this case distribute so that smaller definitions grow faster than
@@ -2151,7 +2151,7 @@ namespace Avalonia.Controls
// and precision of floating-point computation. (However, the resulting
// display is subject to anti-aliasing problems. TANSTAAFL.)
- if (!_AreClose(roundedTakenSize, finalSize))
+ if (!MathUtilities.AreClose(roundedTakenSize, finalSize))
{
// Compute deltas
for (int i = 0; i < definitions.Count; ++i)
@@ -2168,7 +2168,7 @@ namespace Avalonia.Controls
if (roundedTakenSize > finalSize)
{
int i = definitions.Count - 1;
- while ((adjustedSize > finalSize && !_AreClose(adjustedSize, finalSize)) && i >= 0)
+ while ((adjustedSize > finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i >= 0)
{
DefinitionBase definition = definitions[definitionIndices[i]];
double final = definition.SizeCache - dpiIncrement;
@@ -2184,7 +2184,7 @@ namespace Avalonia.Controls
else if (roundedTakenSize < finalSize)
{
int i = 0;
- while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Count)
+ while ((adjustedSize < finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i < definitions.Count)
{
DefinitionBase definition = definitions[definitionIndices[i]];
double final = definition.SizeCache + dpiIncrement;
@@ -2595,27 +2595,6 @@ namespace Avalonia.Controls
set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); }
}
- ///
- /// fp version of d == 0 .
- ///
- /// Value to check.
- /// true if d == 0.
- private static bool _IsZero(double d)
- {
- return (Math.Abs(d) < double.Epsilon);
- }
-
- ///
- /// fp version of d1 == d2
- ///
- /// First value to compare
- /// Second value to compare
- /// true if d1 == d2
- private static bool _AreClose(double d1, double d2)
- {
- return (Math.Abs(d1 - d2) < double.Epsilon);
- }
-
///
/// Returns reference to extended data bag.
///
diff --git a/src/Avalonia.Controls/IndexPath.cs b/src/Avalonia.Controls/IndexPath.cs
index 6c5aaf7ad1..73b75bc23d 100644
--- a/src/Avalonia.Controls/IndexPath.cs
+++ b/src/Avalonia.Controls/IndexPath.cs
@@ -123,6 +123,26 @@ namespace Avalonia.Controls
}
}
+ public bool IsAncestorOf(in IndexPath other)
+ {
+ if (other.GetSize() <= GetSize())
+ {
+ return false;
+ }
+
+ var size = GetSize();
+
+ for (int i = 0; i < size; i++)
+ {
+ if (GetAt(i) != other.GetAt(i))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
public override string ToString()
{
if (_path != null)
diff --git a/src/Avalonia.Controls/IndexRange.cs b/src/Avalonia.Controls/IndexRange.cs
index 1dc161c699..e45d013af4 100644
--- a/src/Avalonia.Controls/IndexRange.cs
+++ b/src/Avalonia.Controls/IndexRange.cs
@@ -132,6 +132,53 @@ namespace Avalonia.Controls
return result;
}
+ public static int Intersect(
+ IList ranges,
+ IndexRange range,
+ IList? removed = null)
+ {
+ var result = 0;
+
+ for (var i = 0; i < ranges.Count && range != s_invalid; ++i)
+ {
+ var existing = ranges[i];
+
+ if (existing.End < range.Begin || existing.Begin > range.End)
+ {
+ removed?.Add(existing);
+ ranges.RemoveAt(i--);
+ result += existing.Count;
+ }
+ else
+ {
+ if (existing.Begin < range.Begin)
+ {
+ var except = new IndexRange(existing.Begin, range.Begin - 1);
+ removed?.Add(except);
+ ranges[i] = existing = new IndexRange(range.Begin, existing.End);
+ result += except.Count;
+ }
+
+ if (existing.End > range.End)
+ {
+ var except = new IndexRange(range.End + 1, existing.End);
+ removed?.Add(except);
+ ranges[i] = new IndexRange(existing.Begin, range.End);
+ result += except.Count;
+ }
+ }
+ }
+
+ MergeRanges(ranges);
+
+ if (removed is object)
+ {
+ MergeRanges(removed);
+ }
+
+ return result;
+ }
+
public static int Remove(
IList ranges,
IndexRange range,
diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs
index 8d48f6646d..83ad2b3638 100644
--- a/src/Avalonia.Controls/LayoutTransformControl.cs
+++ b/src/Avalonia.Controls/LayoutTransformControl.cs
@@ -14,8 +14,8 @@ namespace Avalonia.Controls
///
public class LayoutTransformControl : Decorator
{
- public static readonly StyledProperty LayoutTransformProperty =
- AvaloniaProperty.Register(nameof(LayoutTransform));
+ public static readonly StyledProperty LayoutTransformProperty =
+ AvaloniaProperty.Register(nameof(LayoutTransform));
public static readonly StyledProperty UseRenderTransformProperty =
AvaloniaProperty.Register(nameof(LayoutTransform));
@@ -37,7 +37,7 @@ namespace Avalonia.Controls
///
/// Gets or sets a graphics transformation that should apply to this element when layout is performed.
///
- public Transform LayoutTransform
+ public ITransform LayoutTransform
{
get { return GetValue(LayoutTransformProperty); }
set { SetValue(LayoutTransformProperty, value); }
diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs
index b08519963b..912abc6de3 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -105,6 +105,7 @@ namespace Avalonia.Controls
static MenuItem()
{
SelectableMixin.Attach(IsSelectedProperty);
+ PressedMixin.Attach();
CommandProperty.Changed.Subscribe(CommandChanged);
FocusableProperty.OverrideDefaultValue(true);
HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e));
@@ -534,11 +535,13 @@ namespace Avalonia.Controls
if (oldValue != null)
{
LogicalChildren.Remove(oldValue);
+ PseudoClasses.Remove(":icon");
}
if (newValue != null)
{
LogicalChildren.Add(newValue);
+ PseudoClasses.Add(":icon");
}
}
@@ -566,11 +569,13 @@ namespace Avalonia.Controls
{
RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent));
IsSelected = true;
+ PseudoClasses.Add(":open");
}
else
{
CloseSubmenus();
SelectedIndex = -1;
+ PseudoClasses.Remove(":open");
}
}
diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
index 8c99dffc28..b190c4f2e7 100644
--- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
+++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
@@ -1,5 +1,4 @@
using System;
-using Avalonia.Controls;
namespace Avalonia.Platform
{
@@ -46,9 +45,9 @@ namespace Avalonia.Platform
IPlatformHandle Handle { get; }
///
- /// Gets the maximum size of a window on the system.
+ /// Gets a maximum client size hint for an auto-sizing window, in device-independent pixels.
///
- Size MaxClientSize { get; }
+ Size MaxAutoSizeHint { get; }
///
/// Sets whether this window appears on top of all other windows
diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
index 83ce63f240..3fac440c40 100644
--- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
@@ -113,7 +113,7 @@ namespace Avalonia.Controls.Presenters
{
var scrollable = (ILogicalScrollable)Owner;
var visualRoot = Owner.GetVisualRoot();
- var maxAvailableSize = (visualRoot as WindowBase)?.PlatformImpl?.MaxClientSize
+ var maxAvailableSize = (visualRoot as WindowBase)?.PlatformImpl?.MaxAutoSizeHint
?? (visualRoot as TopLevel)?.ClientSize;
// If infinity is passed as the available size and we're virtualized then we need to
diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs
index 74a3ca8818..e424bf683d 100644
--- a/src/Avalonia.Controls/Primitives/IPopupHost.cs
+++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs
@@ -5,19 +5,70 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
+ ///
+ /// Represents the top-level control opened by a .
+ ///
+ ///
+ /// A popup host can be either be a popup window created by the operating system
+ /// ( ) or an which is created
+ /// on an .
+ ///
public interface IPopupHost : IDisposable
{
+ ///
+ /// Sets the control to display in the popup.
+ ///
+ ///
void SetChild(IControl control);
+
+ ///
+ /// Gets the presenter from the control's template.
+ ///
IContentPresenter Presenter { get; }
+
+ ///
+ /// Gets the root of the visual tree in the case where the popup is presented using a
+ /// separate visual tree.
+ ///
IVisual HostedVisualTreeRoot { get; }
+ ///
+ /// Raised when the control's template is applied.
+ ///
event EventHandler TemplateApplied;
+ ///
+ /// Configures the position of the popup according to a target control and a set of
+ /// placement parameters.
+ ///
+ /// The placement target.
+ /// The placement mode.
+ /// The offset, in device-independent pixels.
+ /// The anchor point.
+ /// The popup gravity.
+ ///
+ /// The anchor rect. If null, the bounds of will be used.
+ ///
void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
- PopupPositioningEdge anchor = PopupPositioningEdge.None,
- PopupPositioningEdge gravity = PopupPositioningEdge.None);
+ PopupAnchor anchor = PopupAnchor.None,
+ PopupGravity gravity = PopupGravity.None,
+ PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
+ Rect? rect = null);
+
+ ///
+ /// Shows the popup.
+ ///
void Show();
+
+ ///
+ /// Hides the popup.
+ ///
void Hide();
+
+ ///
+ /// Binds the constraints of the popup host to a set of properties, usally those present on
+ /// .
+ ///
IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty,
StyledProperty minWidthProperty, StyledProperty maxWidthProperty,
StyledProperty heightProperty, StyledProperty minHeightProperty,
diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
index 3dc9d302db..762d8d37a6 100644
--- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
+++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
@@ -71,10 +71,12 @@ namespace Avalonia.Controls.Primitives
}
public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
- PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None)
+ PopupAnchor anchor = PopupAnchor.None, PopupGravity gravity = PopupGravity.None,
+ PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
+ Rect? rect = null)
{
_positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor,
- gravity);
+ gravity, constraintAdjustment, rect);
UpdatePosition();
}
@@ -122,10 +124,8 @@ namespace Avalonia.Controls.Primitives
}, DispatcherPriority.Layout);
}
- Point IManagedPopupPositionerPopup.TranslatePoint(Point pt) => pt;
-
- Size IManagedPopupPositionerPopup.TranslateSize(Size size) => size;
-
+ double IManagedPopupPositionerPopup.Scaling => 1;
+
public static IPopupHost CreatePopupHost(IVisual target, IAvaloniaDependencyResolver dependencyResolver)
{
var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup();
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index 49315e1b25..ac4f805174 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
@@ -37,12 +38,45 @@ namespace Avalonia.Controls.Primitives
o => o.IsOpen,
(o, v) => o.IsOpen = v);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementAnchorProperty =
+ AvaloniaProperty.Register(nameof(PlacementAnchor));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementConstraintAdjustmentProperty =
+ AvaloniaProperty.Register(
+ nameof(PlacementConstraintAdjustment),
+ PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY |
+ PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementGravityProperty =
+ AvaloniaProperty.Register(nameof(PlacementGravity));
+
///
/// Defines the property.
///
public static readonly StyledProperty PlacementModeProperty =
AvaloniaProperty.Register(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementRectProperty =
+ AvaloniaProperty.Register(nameof(PlacementRect));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementTargetProperty =
+ AvaloniaProperty.Register(nameof(PlacementTarget));
+
#pragma warning disable 618
///
/// Defines the property.
@@ -63,12 +97,6 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty VerticalOffsetProperty =
AvaloniaProperty.Register(nameof(VerticalOffset));
- ///
- /// Defines the property.
- ///
- public static readonly StyledProperty PlacementTargetProperty =
- AvaloniaProperty.Register(nameof(PlacementTarget));
-
///
/// Defines the property.
///
@@ -145,6 +173,36 @@ namespace Avalonia.Controls.Primitives
set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
}
+ ///
+ /// Gets or sets the anchor point on the when
+ /// is .
+ ///
+ public PopupAnchor PlacementAnchor
+ {
+ get { return GetValue(PlacementAnchorProperty); }
+ set { SetValue(PlacementAnchorProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value describing how the popup position will be adjusted if the
+ /// unadjusted position would result in the popup being partly constrained.
+ ///
+ public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
+ {
+ get { return GetValue(PlacementConstraintAdjustmentProperty); }
+ set { SetValue(PlacementConstraintAdjustmentProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value which defines in what direction the popup should open
+ /// when is .
+ ///
+ public PopupGravity PlacementGravity
+ {
+ get { return GetValue(PlacementGravityProperty); }
+ set { SetValue(PlacementGravityProperty, value); }
+ }
+
///
/// Gets or sets the placement mode of the popup in relation to the .
///
@@ -154,6 +212,32 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementModeProperty, value); }
}
+ ///
+ /// Gets or sets the the anchor rectangle within the parent that the popup will be placed
+ /// relative to when is .
+ ///
+ ///
+ /// The placement rect defines a rectangle relative to around
+ /// which the popup will be opened, with determining which edge
+ /// of the placement target is used.
+ ///
+ /// If unset, the anchor rectangle will be the bounds of the .
+ ///
+ public Rect? PlacementRect
+ {
+ get { return GetValue(PlacementRectProperty); }
+ set { SetValue(PlacementRectProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the control that is used to determine the popup's position.
+ ///
+ public Control? PlacementTarget
+ {
+ get { return GetValue(PlacementTargetProperty); }
+ set { SetValue(PlacementTargetProperty, value); }
+ }
+
[Obsolete("This property has no effect")]
public bool ObeyScreenEdges
{
@@ -162,7 +246,7 @@ namespace Avalonia.Controls.Primitives
}
///
- /// Gets or sets the Horizontal offset of the popup in relation to the
+ /// Gets or sets the Horizontal offset of the popup in relation to the .
///
public double HorizontalOffset
{
@@ -171,7 +255,7 @@ namespace Avalonia.Controls.Primitives
}
///
- /// Gets or sets the Vertical offset of the popup in relation to the
+ /// Gets or sets the Vertical offset of the popup in relation to the .
///
public double VerticalOffset
{
@@ -179,15 +263,6 @@ namespace Avalonia.Controls.Primitives
set { SetValue(VerticalOffsetProperty, value); }
}
- ///
- /// Gets or sets the control that is used to determine the popup's position.
- ///
- public Control? PlacementTarget
- {
- get { return GetValue(PlacementTargetProperty); }
- set { SetValue(PlacementTargetProperty, value); }
- }
-
///
/// Gets or sets a value indicating whether the popup should stay open when the popup is
/// pressed or loses focus.
@@ -260,8 +335,12 @@ namespace Avalonia.Controls.Primitives
popupHost.ConfigurePosition(
placementTarget,
- PlacementMode,
- new Point(HorizontalOffset, VerticalOffset));
+ PlacementMode,
+ new Point(HorizontalOffset, VerticalOffset),
+ PlacementAnchor,
+ PlacementGravity,
+ PlacementConstraintAdjustment,
+ PlacementRect);
DeferCleanup(SubscribeToEventHandler>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler,
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
index f0358ec04f..aed7dff0fe 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
@@ -50,46 +50,48 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives.PopupPositioning
{
///
- ///
- /// The IPopupPositioner provides a collection of rules for the placement of a
- /// a popup relative to its parent. Rules can be defined to ensure
- /// the popup remains within the visible area's borders, and to
- /// specify how the popup changes its position, such as sliding along
- /// an axis, or flipping around a rectangle. These positioner-created rules are
- /// constrained by the requirement that a popup must intersect with or
- /// be at least partially adjacent to its parent surface.
+ /// Provides positioning parameters to .
///
+ ///
+ /// The IPopupPositioner provides a collection of rules for the placement of a a popup relative
+ /// to its parent. Rules can be defined to ensure the popup remains within the visible area's
+ /// borders, and to specify how the popup changes its position, such as sliding along an axis,
+ /// or flipping around a rectangle. These positioner-created rules are constrained by the
+ /// requirement that a popup must intersect with or be at least partially adjacent to its parent
+ /// surface.
+ ///
public struct PopupPositionerParameters
{
- private PopupPositioningEdge _gravity;
- private PopupPositioningEdge _anchor;
+ private PopupGravity _gravity;
+ private PopupAnchor _anchor;
///
- /// Set the size of the popup that is to be positioned with the positioner
- /// object. The size is in scaled coordinates.
+ /// Set the size of the popup that is to be positioned with the positioner object, in device-
+ /// independent pixels.
///
public Size Size { get; set; }
///
- /// Specify the anchor rectangle within the parent that the popup
- /// will be placed relative to. The rectangle is relative to the
- /// parent geometry
- ///
- /// The anchor rectangle may not extend outside the window geometry of the
- /// popup's parent. The anchor rectangle is in scaled coordinates
+ /// Specifies the anchor rectangle within the parent that the popup will be placed relative
+ /// to, in device-independent pixels.
///
+ ///
+ /// The rectangle is relative to the parent geometry and may not extend outside the window
+ /// geometry of the popup's parent.
+ ///
public Rect AnchorRectangle { get; set; }
-
///
- /// Defines the anchor point for the anchor rectangle. The specified anchor
- /// is used derive an anchor point that the popup will be
- /// positioned relative to. If a corner anchor is set (e.g. 'TopLeft' or
- /// 'BottomRight'), the anchor point will be at the specified corner;
- /// otherwise, the derived anchor point will be centered on the specified
- /// edge, or in the center of the anchor rectangle if no edge is specified.
+ /// Defines the anchor point for the anchor rectangle.
///
- public PopupPositioningEdge Anchor
+ ///
+ /// The specified anchor is used derive an anchor point that the popup will be positioned
+ /// relative to. If a corner anchor is set (e.g. 'TopLeft' or 'BottomRight'), the anchor
+ /// point will be at the specified corner; otherwise, the derived anchor point will be
+ /// centered on the specified edge, or in the center of the anchor rectangle if no edge is
+ /// specified.
+ ///
+ public PopupAnchor Anchor
{
get => _anchor;
set
@@ -100,66 +102,70 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
}
///
- /// Defines in what direction a popup should be positioned, relative to
- /// the anchor point of the parent. If a corner gravity is
- /// specified (e.g. 'BottomRight' or 'TopLeft'), then the popup
- /// will be placed towards the specified gravity; otherwise, the popup
- /// will be centered over the anchor point on any axis that had no
- /// gravity specified.
+ /// Defines in what direction a popup should be positioned, relative to the anchor point of
+ /// the parent.
///
- public PopupPositioningEdge Gravity
+ ///
+ /// If a corner gravity is specified (e.g. 'BottomRight' or 'TopLeft'), then the popup will
+ /// be placed towards the specified gravity; otherwise, the popup will be centered over the
+ /// anchor point on any axis that had no gravity specified.
+ ///
+ public PopupGravity Gravity
{
get => _gravity;
set
{
- PopupPositioningEdgeHelper.ValidateEdge(value);
+ PopupPositioningEdgeHelper.ValidateGravity(value);
_gravity = value;
}
}
///
- /// Specify how the popup should be positioned if the originally intended
- /// position caused the popup to be constrained, meaning at least
- /// partially outside positioning boundaries set by the positioner. The
- /// adjustment is set by constructing a bitmask describing the adjustment to
- /// be made when the popup is constrained on that axis.
+ /// Specify how the popup should be positioned if the originally intended position caused
+ /// the popup to be constrained.
+ ///
+ ///
+ /// Adjusts the popup position if the intended position caused the popup to be constrained;
+ /// meaning at least partially outside positioning boundaries set by the positioner. The
+ /// adjustment is set by constructing a bitmask describing the adjustment to be made when
+ /// the popup is constrained on that axis.
///
- /// If no bit for one axis is set, the positioner will assume that the child
- /// surface should not change its position on that axis when constrained.
+ /// If no bit for one axis is set, the positioner will assume that the child surface should
+ /// not change its position on that axis when constrained.
///
- /// If more than one bit for one axis is set, the order of how adjustments
- /// are applied is specified in the corresponding adjustment descriptions.
+ /// If more than one bit for one axis is set, the order of how adjustments are applied is
+ /// specified in the corresponding adjustment descriptions.
///
/// The default adjustment is none.
- ///
+ ///
public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; }
-
+
///
/// Specify the popup position offset relative to the position of the
- /// anchor on the anchor rectangle and the anchor on the popup. For
- /// example if the anchor of the anchor rectangle is at (x, y), the popup
- /// has the gravity bottom|right, and the offset is (ox, oy), the calculated
- /// surface position will be (x + ox, y + oy). The offset position of the
- /// surface is the one used for constraint testing. See
- /// set_constraint_adjustment.
- ///
- /// An example use case is placing a popup menu on top of a user interface
- /// element, while aligning the user interface element of the parent surface
- /// with some user interface element placed somewhere in the popup.
+ /// anchor on the anchor rectangle and the anchor on the popup.
///
+ ///
+ /// For example if the anchor of the anchor rectangle is at (x, y), the popup has the
+ /// gravity bottom|right, and the offset is (ox, oy), the calculated surface position will
+ /// be (x + ox, y + oy). The offset position of the surface is the one used for constraint
+ /// testing. See set_constraint_adjustment.
+ ///
+ /// An example use case is placing a popup menu on top of a user interface element, while
+ /// aligning the user interface element of the parent surface with some user interface
+ /// element placed somewhere in the popup.
+ ///
public Point Offset { get; set; }
}
-
+
///
- /// The constraint adjustment value define ways how popup position will
- /// be adjusted if the unadjusted position would result in the popup
- /// being partly constrained.
- ///
- /// Whether a popup is considered 'constrained' is left to the positioner
- /// to determine. For example, the popup may be partly outside the
- /// target platform defined 'work area', thus necessitating the popup's
- /// position be adjusted until it is entirely inside the work area.
+ /// Defines how a popup position will be adjusted if the unadjusted position would result in
+ /// the popup being partly constrained.
///
+ ///
+ /// Whether a popup is considered 'constrained' is left to the positioner to determine. For
+ /// example, the popup may be partly outside the target platform defined 'work area', thus
+ /// necessitating the popup's position be adjusted until it is entirely inside the work area.
+ ///
[Flags]
public enum PopupPositionerConstraintAdjustment
{
@@ -171,79 +177,97 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
///
/// Slide the surface along the x axis until it is no longer constrained.
- /// First try to slide towards the direction of the gravity on the x axis
- /// until either the edge in the opposite direction of the gravity is
- /// unconstrained or the edge in the direction of the gravity is
- /// constrained.
- ///
- /// Then try to slide towards the opposite direction of the gravity on the
- /// x axis until either the edge in the direction of the gravity is
- /// unconstrained or the edge in the opposite direction of the gravity is
- /// constrained.
///
+ ///
+ /// First try to slide towards the direction of the gravity on the x axis until either the
+ /// edge in the opposite direction of the gravity is unconstrained or the edge in the
+ /// direction of the gravity is constrained.
+ ///
+ /// Then try to slide towards the opposite direction of the gravity on the x axis until
+ /// either the edge in the direction of the gravity is unconstrained or the edge in the
+ /// opposite direction of the gravity is constrained.
+ ///
SlideX = 1,
-
///
- /// Slide the surface along the y axis until it is no longer constrained.
- ///
- /// First try to slide towards the direction of the gravity on the y axis
- /// until either the edge in the opposite direction of the gravity is
- /// unconstrained or the edge in the direction of the gravity is
- /// constrained.
- ///
- /// Then try to slide towards the opposite direction of the gravity on the
- /// y axis until either the edge in the direction of the gravity is
- /// unconstrained or the edge in the opposite direction of the gravity is
- /// constrained.
- /// */
+ /// Slide the surface along the y axis until it is no longer constrained.
///
+ ///
+ /// First try to slide towards the direction of the gravity on the y axis until either the
+ /// edge in the opposite direction of the gravity is unconstrained or the edge in the
+ /// direction of the gravity is constrained.
+ ///
+ /// Then try to slide towards the opposite direction of the gravity on the y axis until
+ /// either the edge in the direction of the gravity is unconstrained or the edge in the
+ /// opposite direction of the gravity is constrained.
+ ///
SlideY = 2,
///
- /// Invert the anchor and gravity on the x axis if the surface is
- /// constrained on the x axis. For example, if the left edge of the
- /// surface is constrained, the gravity is 'left' and the anchor is
- /// 'left', change the gravity to 'right' and the anchor to 'right'.
- ///
- /// If the adjusted position also ends up being constrained, the resulting
- /// position of the flip_x adjustment will be the one before the
- /// adjustment.
+ /// Invert the anchor and gravity on the x axis if the surface is constrained on the x axis.
///
+ ///
+ /// For example, if the left edge of the surface is constrained, the gravity is 'left' and
+ /// the anchor is 'left', change the gravity to 'right' and the anchor to 'right'.
+ ///
+ /// If the adjusted position also ends up being constrained, the resulting position of the
+ /// FlipX adjustment will be the one before the adjustment.
+ /// ///
FlipX = 4,
///
- /// Invert the anchor and gravity on the y axis if the surface is
- /// constrained on the y axis. For example, if the bottom edge of the
- /// surface is constrained, the gravity is 'bottom' and the anchor is
- /// 'bottom', change the gravity to 'top' and the anchor to 'top'.
+ /// Invert the anchor and gravity on the y axis if the surface is constrained on the y axis.
+ ///
+ ///
+ /// For example, if the bottom edge of the surface is constrained, the gravity is 'bottom'
+ /// and the anchor is 'bottom', change the gravity to 'top' and the anchor to 'top'.
///
- /// The adjusted position is calculated given the original anchor
- /// rectangle and offset, but with the new flipped anchor and gravity
- /// values.
+ /// The adjusted position is calculated given the original anchor rectangle and offset, but
+ /// with the new flipped anchor and gravity values.
///
- /// If the adjusted position also ends up being constrained, the resulting
- /// position of the flip_y adjustment will be the one before the
- /// adjustment.
- ///
+ /// If the adjusted position also ends up being constrained, the resulting position of the
+ /// FlipY adjustment will be the one before the adjustment.
+ ///
FlipY = 8,
- All = SlideX|SlideY|FlipX|FlipY
+
+ ///
+ /// Horizontally resize the surface
+ ///
+ ///
+ /// Resize the surface horizontally so that it is completely unconstrained.
+ ///
+ ResizeX = 16,
+
+ ///
+ /// Vertically resize the surface
+ ///
+ ///
+ /// Resize the surface vertically so that it is completely unconstrained.
+ ///
+ ResizeY = 16,
+
+ All = SlideX|SlideY|FlipX|FlipY|ResizeX|ResizeY
}
static class PopupPositioningEdgeHelper
{
- public static void ValidateEdge(this PopupPositioningEdge edge)
+ public static void ValidateEdge(this PopupAnchor edge)
{
- if (((edge & PopupPositioningEdge.Left) != 0 && (edge & PopupPositioningEdge.Right) != 0)
+ if (((edge & PopupAnchor.Left) != 0 && (edge & PopupAnchor.Right) != 0)
||
- ((edge & PopupPositioningEdge.Top) != 0 && (edge & PopupPositioningEdge.Bottom) != 0))
+ ((edge & PopupAnchor.Top) != 0 && (edge & PopupAnchor.Bottom) != 0))
throw new ArgumentException("Opposite edges specified");
}
- public static PopupPositioningEdge Flip(this PopupPositioningEdge edge)
+ public static void ValidateGravity(this PopupGravity gravity)
+ {
+ ValidateEdge((PopupAnchor)gravity);
+ }
+
+ public static PopupAnchor Flip(this PopupAnchor edge)
{
- var hmask = PopupPositioningEdge.Left | PopupPositioningEdge.Right;
- var vmask = PopupPositioningEdge.Top | PopupPositioningEdge.Bottom;
+ var hmask = PopupAnchor.Left | PopupAnchor.Right;
+ var vmask = PopupAnchor.Top | PopupAnchor.Bottom;
if ((edge & hmask) != 0)
edge ^= hmask;
if ((edge & vmask) != 0)
@@ -251,43 +275,167 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
return edge;
}
- public static PopupPositioningEdge FlipX(this PopupPositioningEdge edge)
+ public static PopupAnchor FlipX(this PopupAnchor edge)
{
- if ((edge & PopupPositioningEdge.HorizontalMask) != 0)
- edge ^= PopupPositioningEdge.HorizontalMask;
+ if ((edge & PopupAnchor.HorizontalMask) != 0)
+ edge ^= PopupAnchor.HorizontalMask;
return edge;
}
- public static PopupPositioningEdge FlipY(this PopupPositioningEdge edge)
+ public static PopupAnchor FlipY(this PopupAnchor edge)
{
- if ((edge & PopupPositioningEdge.VerticalMask) != 0)
- edge ^= PopupPositioningEdge.VerticalMask;
+ if ((edge & PopupAnchor.VerticalMask) != 0)
+ edge ^= PopupAnchor.VerticalMask;
return edge;
}
-
+
+ public static PopupGravity FlipX(this PopupGravity gravity)
+ {
+ return (PopupGravity)FlipX((PopupAnchor)gravity);
+ }
+
+ public static PopupGravity FlipY(this PopupGravity gravity)
+ {
+ return (PopupGravity)FlipY((PopupAnchor)gravity);
+ }
}
+ ///
+ /// Defines the edges around an anchor rectangle on which a popup will open.
+ ///
[Flags]
- public enum PopupPositioningEdge
+ public enum PopupAnchor
{
+ ///
+ /// The center of the anchor rectangle.
+ ///
None,
+
+ ///
+ /// The top edge of the anchor rectangle.
+ ///
Top = 1,
+
+ ///
+ /// The bottom edge of the anchor rectangle.
+ ///
Bottom = 2,
+
+ ///
+ /// The left edge of the anchor rectangle.
+ ///
Left = 4,
+
+ ///
+ /// The right edge of the anchor rectangle.
+ ///
Right = 8,
+
+ ///
+ /// The top-left corner of the anchor rectangle.
+ ///
TopLeft = Top | Left,
+
+ ///
+ /// The top-right corner of the anchor rectangle.
+ ///
TopRight = Top | Right,
+
+ ///
+ /// The bottom-left corner of the anchor rectangle.
+ ///
BottomLeft = Bottom | Left,
+
+ ///
+ /// The bottom-right corner of the anchor rectangle.
+ ///
BottomRight = Bottom | Right,
-
+ ///
+ /// A mask for the vertical component flags.
+ ///
VerticalMask = Top | Bottom,
+
+ ///
+ /// A mask for the horizontal component flags.
+ ///
HorizontalMask = Left | Right,
+
+ ///
+ /// A mask for all flags.
+ ///
AllMask = VerticalMask|HorizontalMask
}
+ ///
+ /// Defines the direction in which a popup will open.
+ ///
+ [Flags]
+ public enum PopupGravity
+ {
+ ///
+ /// The popup will be centered over the anchor edge.
+ ///
+ None,
+
+ ///
+ /// The popup will be positioned above the anchor edge
+ ///
+ Top = 1,
+
+ ///
+ /// The popup will be positioned below the anchor edge
+ ///
+ Bottom = 2,
+
+ ///
+ /// The popup will be positioned to the left of the anchor edge
+ ///
+ Left = 4,
+
+ ///
+ /// The popup will be positioned to the right of the anchor edge
+ ///
+ Right = 8,
+
+ ///
+ /// The popup will be positioned to the top-left of the anchor edge
+ ///
+ TopLeft = Top | Left,
+
+ ///
+ /// The popup will be positioned to the top-right of the anchor edge
+ ///
+ TopRight = Top | Right,
+
+ ///
+ /// The popup will be positioned to the bottom-left of the anchor edge
+ ///
+ BottomLeft = Bottom | Left,
+
+ ///
+ /// The popup will be positioned to the bottom-right of the anchor edge
+ ///
+ BottomRight = Bottom | Right,
+ }
+
+ ///
+ /// Positions an .
+ ///
+ ///
+ /// is an abstraction of the wayland xdg_positioner spec.
+ ///
+ /// The popup positioner implementation is determined by the platform implementation. A default
+ /// managed implementation is provided in for platforms
+ /// on which popups can be arbitrarily positioned.
+ ///
public interface IPopupPositioner
{
+ ///
+ /// Updates the position of the associated according to the
+ /// specified parameters.
+ ///
+ /// The positioning parameters.
void Update(PopupPositionerParameters parameters);
}
@@ -296,18 +444,19 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters,
TopLevel topLevel,
IVisual target, PlacementMode placement, Point offset,
- PopupPositioningEdge anchor, PopupPositioningEdge gravity)
+ PopupAnchor anchor, PopupGravity gravity,
+ PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect)
{
// We need a better way for tracking the last pointer position
var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position);
positionerParameters.Offset = offset;
- positionerParameters.ConstraintAdjustment = PopupPositionerConstraintAdjustment.All;
+ positionerParameters.ConstraintAdjustment = constraintAdjustment;
if (placement == PlacementMode.Pointer)
{
positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1));
- positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
- positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
+ positionerParameters.Anchor = PopupAnchor.TopLeft;
+ positionerParameters.Gravity = PopupGravity.BottomRight;
}
else
{
@@ -317,32 +466,33 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
if (matrix == null)
{
if (target.GetVisualRoot() == null)
- throw new InvalidCastException("Target control is not attached to the visual tree");
- throw new InvalidCastException("Target control is not in the same tree as the popup parent");
+ throw new InvalidOperationException("Target control is not attached to the visual tree");
+ throw new InvalidOperationException("Target control is not in the same tree as the popup parent");
}
- positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size)
- .TransformToAABB(matrix.Value);
+ var bounds = new Rect(default, target.Bounds.Size);
+ var anchorRect = rect ?? bounds;
+ positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
if (placement == PlacementMode.Right)
{
- positionerParameters.Anchor = PopupPositioningEdge.TopRight;
- positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
+ positionerParameters.Anchor = PopupAnchor.TopRight;
+ positionerParameters.Gravity = PopupGravity.BottomRight;
}
else if (placement == PlacementMode.Bottom)
{
- positionerParameters.Anchor = PopupPositioningEdge.BottomLeft;
- positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
+ positionerParameters.Anchor = PopupAnchor.BottomLeft;
+ positionerParameters.Gravity = PopupGravity.BottomRight;
}
else if (placement == PlacementMode.Left)
{
- positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
- positionerParameters.Gravity = PopupPositioningEdge.BottomLeft;
+ positionerParameters.Anchor = PopupAnchor.TopLeft;
+ positionerParameters.Gravity = PopupGravity.BottomLeft;
}
else if (placement == PlacementMode.Top)
{
- positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
- positionerParameters.Gravity = PopupPositioningEdge.TopRight;
+ positionerParameters.Anchor = PopupAnchor.TopLeft;
+ positionerParameters.Gravity = PopupGravity.TopRight;
}
else if (placement == PlacementMode.AnchorAndGravity)
{
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
index 07348cdf78..8c464c7aad 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Transactions;
namespace Avalonia.Controls.Primitives.PopupPositioning
{
@@ -8,9 +9,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
{
IReadOnlyList Screens { get; }
Rect ParentClientAreaScreenGeometry { get; }
+ double Scaling { get; }
void MoveAndResize(Point devicePoint, Size virtualSize);
- Point TranslatePoint(Point pt);
- Size TranslateSize(Size size);
}
public class ManagedPopupPositionerScreenInfo
@@ -25,6 +25,10 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
}
}
+ ///
+ /// An implementation for platforms on which a popup can be
+ /// aritrarily positioned.
+ ///
public class ManagedPopupPositioner : IPopupPositioner
{
private readonly IManagedPopupPositionerPopup _popup;
@@ -35,38 +39,38 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
}
- private static Point GetAnchorPoint(Rect anchorRect, PopupPositioningEdge edge)
+ private static Point GetAnchorPoint(Rect anchorRect, PopupAnchor edge)
{
double x, y;
- if ((edge & PopupPositioningEdge.Left) != 0)
+ if ((edge & PopupAnchor.Left) != 0)
x = anchorRect.X;
- else if ((edge & PopupPositioningEdge.Right) != 0)
+ else if ((edge & PopupAnchor.Right) != 0)
x = anchorRect.Right;
else
x = anchorRect.X + anchorRect.Width / 2;
- if ((edge & PopupPositioningEdge.Top) != 0)
+ if ((edge & PopupAnchor.Top) != 0)
y = anchorRect.Y;
- else if ((edge & PopupPositioningEdge.Bottom) != 0)
+ else if ((edge & PopupAnchor.Bottom) != 0)
y = anchorRect.Bottom;
else
y = anchorRect.Y + anchorRect.Height / 2;
return new Point(x, y);
}
- private static Point Gravitate(Point anchorPoint, Size size, PopupPositioningEdge gravity)
+ private static Point Gravitate(Point anchorPoint, Size size, PopupGravity gravity)
{
double x, y;
- if ((gravity & PopupPositioningEdge.Left) != 0)
+ if ((gravity & PopupGravity.Left) != 0)
x = -size.Width;
- else if ((gravity & PopupPositioningEdge.Right) != 0)
+ else if ((gravity & PopupGravity.Right) != 0)
x = 0;
else
x = -size.Width / 2;
- if ((gravity & PopupPositioningEdge.Top) != 0)
+ if ((gravity & PopupGravity.Top) != 0)
y = -size.Height;
- else if ((gravity & PopupPositioningEdge.Bottom) != 0)
+ else if ((gravity & PopupGravity.Bottom) != 0)
y = 0;
else
y = -size.Height / 2;
@@ -75,17 +79,24 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
public void Update(PopupPositionerParameters parameters)
{
-
- Update(_popup.TranslateSize(parameters.Size), parameters.Size,
- new Rect(_popup.TranslatePoint(parameters.AnchorRectangle.TopLeft),
- _popup.TranslateSize(parameters.AnchorRectangle.Size)),
- parameters.Anchor, parameters.Gravity, parameters.ConstraintAdjustment,
- _popup.TranslatePoint(parameters.Offset));
+ var rect = Calculate(
+ parameters.Size * _popup.Scaling,
+ new Rect(
+ parameters.AnchorRectangle.TopLeft * _popup.Scaling,
+ parameters.AnchorRectangle.Size * _popup.Scaling),
+ parameters.Anchor,
+ parameters.Gravity,
+ parameters.ConstraintAdjustment,
+ parameters.Offset * _popup.Scaling);
+
+ _popup.MoveAndResize(
+ rect.Position,
+ rect.Size / _popup.Scaling);
}
- private void Update(Size translatedSize, Size originalSize,
- Rect anchorRect, PopupPositioningEdge anchor, PopupPositioningEdge gravity,
+ private Rect Calculate(Size translatedSize,
+ Rect anchorRect, PopupAnchor anchor, PopupGravity gravity,
PopupPositionerConstraintAdjustment constraintAdjustment, Point offset)
{
var parentGeometry = _popup.ParentClientAreaScreenGeometry;
@@ -112,28 +123,30 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
var bounds = GetBounds();
- bool FitsInBounds(Rect rc, PopupPositioningEdge edge = PopupPositioningEdge.AllMask)
+ bool FitsInBounds(Rect rc, PopupAnchor edge = PopupAnchor.AllMask)
{
- if ((edge & PopupPositioningEdge.Left) != 0
+ if ((edge & PopupAnchor.Left) != 0
&& rc.X < bounds.X)
return false;
- if ((edge & PopupPositioningEdge.Top) != 0
+ if ((edge & PopupAnchor.Top) != 0
&& rc.Y < bounds.Y)
return false;
- if ((edge & PopupPositioningEdge.Right) != 0
+ if ((edge & PopupAnchor.Right) != 0
&& rc.Right > bounds.Right)
return false;
- if ((edge & PopupPositioningEdge.Bottom) != 0
+ if ((edge & PopupAnchor.Bottom) != 0
&& rc.Bottom > bounds.Bottom)
return false;
return true;
}
- Rect GetUnconstrained(PopupPositioningEdge a, PopupPositioningEdge g) =>
+ static bool IsValid(in Rect rc) => rc.Width > 0 && rc.Height > 0;
+
+ Rect GetUnconstrained(PopupAnchor a, PopupGravity g) =>
new Rect(Gravitate(GetAnchorPoint(anchorRect, a), translatedSize, g) + offset, translatedSize);
@@ -141,11 +154,11 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
// If flipping geometry and anchor is allowed and helps, use the flipped one,
// otherwise leave it as is
- if (!FitsInBounds(geo, PopupPositioningEdge.HorizontalMask)
+ if (!FitsInBounds(geo, PopupAnchor.HorizontalMask)
&& (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0)
{
var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX());
- if (FitsInBounds(flipped, PopupPositioningEdge.HorizontalMask))
+ if (FitsInBounds(flipped, PopupAnchor.HorizontalMask))
geo = geo.WithX(flipped.X);
}
@@ -157,13 +170,34 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
geo = geo.WithX(bounds.Right - geo.Width);
}
+ // Resize the rect horizontally if allowed.
+ if ((constraintAdjustment & PopupPositionerConstraintAdjustment.ResizeX) != 0)
+ {
+ var unconstrainedRect = geo;
+
+ if (!FitsInBounds(unconstrainedRect, PopupAnchor.Left))
+ {
+ unconstrainedRect = unconstrainedRect.WithX(bounds.X);
+ }
+
+ if (!FitsInBounds(unconstrainedRect, PopupAnchor.Right))
+ {
+ unconstrainedRect = unconstrainedRect.WithWidth(bounds.Width - unconstrainedRect.X);
+ }
+
+ if (IsValid(unconstrainedRect))
+ {
+ geo = unconstrainedRect;
+ }
+ }
+
// If flipping geometry and anchor is allowed and helps, use the flipped one,
// otherwise leave it as is
- if (!FitsInBounds(geo, PopupPositioningEdge.VerticalMask)
+ if (!FitsInBounds(geo, PopupAnchor.VerticalMask)
&& (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0)
{
var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY());
- if (FitsInBounds(flipped, PopupPositioningEdge.VerticalMask))
+ if (FitsInBounds(flipped, PopupAnchor.VerticalMask))
geo = geo.WithY(flipped.Y);
}
@@ -175,7 +209,28 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
geo = geo.WithY(bounds.Bottom - geo.Height);
}
- _popup.MoveAndResize(geo.TopLeft, originalSize);
+ // Resize the rect vertically if allowed.
+ if ((constraintAdjustment & PopupPositionerConstraintAdjustment.ResizeY) != 0)
+ {
+ var unconstrainedRect = geo;
+
+ if (!FitsInBounds(unconstrainedRect, PopupAnchor.Top))
+ {
+ unconstrainedRect = unconstrainedRect.WithY(bounds.Y);
+ }
+
+ if (!FitsInBounds(unconstrainedRect, PopupAnchor.Bottom))
+ {
+ unconstrainedRect = unconstrainedRect.WithHeight(bounds.Height - unconstrainedRect.Y);
+ }
+
+ if (IsValid(unconstrainedRect))
+ {
+ geo = unconstrainedRect;
+ }
+ }
+
+ return geo;
}
}
}
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
index 8e7e429a73..b0e3d1ab08 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
@@ -32,7 +32,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
{
// Popup positioner operates with abstract coordinates, but in our case they are pixel ones
var point = _parent.PointToScreen(default);
- var size = TranslateSize(_parent.ClientSize);
+ var size = _parent.ClientSize * Scaling;
return new Rect(point.X, point.Y, size.Width, size.Height);
}
@@ -43,8 +43,6 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
_moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling);
}
- public virtual Point TranslatePoint(Point pt) => pt * _parent.Scaling;
-
- public virtual Size TranslateSize(Size size) => size * _parent.Scaling;
+ public virtual double Scaling => _parent.Scaling;
}
}
diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs
index aab7a68795..da7352b77f 100644
--- a/src/Avalonia.Controls/Primitives/PopupRoot.cs
+++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs
@@ -82,11 +82,13 @@ namespace Avalonia.Controls.Primitives
}
public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
- PopupPositioningEdge anchor = PopupPositioningEdge.None,
- PopupPositioningEdge gravity = PopupPositioningEdge.None)
+ PopupAnchor anchor = PopupAnchor.None,
+ PopupGravity gravity = PopupGravity.None,
+ PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
+ Rect? rect = null)
{
_positionerParameters.ConfigurePosition(_parent, target,
- placement, offset, anchor, gravity);
+ placement, offset, anchor, gravity, constraintAdjustment, rect);
if (_positionerParameters.Size != default)
UpdatePosition();
@@ -119,7 +121,20 @@ namespace Avalonia.Controls.Primitives
protected override Size MeasureOverride(Size availableSize)
{
- var measured = base.MeasureOverride(availableSize);
+ var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity;
+ var constraint = availableSize;
+
+ if (double.IsInfinity(constraint.Width))
+ {
+ constraint = constraint.WithWidth(maxAutoSize.Width);
+ }
+
+ if (double.IsInfinity(constraint.Height))
+ {
+ constraint = constraint.WithHeight(maxAutoSize.Height);
+ }
+
+ var measured = base.MeasureOverride(constraint);
var width = measured.Width;
var height = measured.Height;
var widthCache = Width;
diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs
index e104a8a664..c91adaa26e 100644
--- a/src/Avalonia.Controls/Primitives/Track.cs
+++ b/src/Avalonia.Controls/Primitives/Track.cs
@@ -41,13 +41,16 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty IsDirectionReversedProperty =
AvaloniaProperty.Register(nameof(IsDirectionReversed));
+ public static readonly StyledProperty IgnoreThumbDragProperty =
+ AvaloniaProperty.Register(nameof(IsThumbDragHandled));
+
private double _minimum;
private double _maximum = 100.0;
private double _value;
static Track()
{
- ThumbProperty.Changed.AddClassHandler((x,e) => x.ThumbChanged(e));
+ ThumbProperty.Changed.AddClassHandler((x, e) => x.ThumbChanged(e));
IncreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e));
DecreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e));
AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty);
@@ -113,6 +116,12 @@ namespace Avalonia.Controls.Primitives
set { SetValue(IsDirectionReversedProperty, value); }
}
+ public bool IsThumbDragHandled
+ {
+ get { return GetValue(IgnoreThumbDragProperty); }
+ set { SetValue(IgnoreThumbDragProperty, value); }
+ }
+
private double ThumbCenterOffset { get; set; }
private double Density { get; set; }
@@ -250,7 +259,7 @@ namespace Avalonia.Controls.Primitives
CoerceLength(ref increaseButtonLength, arrangeSize.Width);
CoerceLength(ref thumbLength, arrangeSize.Width);
- offset = offset.WithY(isDirectionReversed ? increaseButtonLength + thumbLength : 0.0);
+ offset = offset.WithX(isDirectionReversed ? increaseButtonLength + thumbLength : 0.0);
pieceSize = pieceSize.WithWidth(decreaseButtonLength);
if (DecreaseButton != null)
@@ -422,6 +431,9 @@ namespace Avalonia.Controls.Primitives
private void ThumbDragged(object sender, VectorEventArgs e)
{
+ if (IsThumbDragHandled)
+ return;
+
Value = MathUtilities.Clamp(
Value + ValueFromDistance(e.Vector.X, e.Vector.Y),
Minimum,
diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs
index c3f0dc0056..813d6967c3 100644
--- a/src/Avalonia.Controls/ScrollViewer.cs
+++ b/src/Avalonia.Controls/ScrollViewer.cs
@@ -387,6 +387,38 @@ namespace Avalonia.Controls
///
IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement
+ ///
+ /// Scrolls the content up one line.
+ ///
+ public void LineUp()
+ {
+ Offset -= new Vector(0, _smallChange.Height);
+ }
+
+ ///
+ /// Scrolls the content down one line.
+ ///
+ public void LineDown()
+ {
+ Offset += new Vector(0, _smallChange.Height);
+ }
+
+ ///
+ /// Scrolls the content left one line.
+ ///
+ public void LineLeft()
+ {
+ Offset -= new Vector(_smallChange.Width, 0);
+ }
+
+ ///
+ /// Scrolls the content right one line.
+ ///
+ public void LineRight()
+ {
+ Offset += new Vector(_smallChange.Width, 0);
+ }
+
///
/// Scrolls to the top-left corner of the content.
///
diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs
index 93699583e6..ff1c0260bb 100644
--- a/src/Avalonia.Controls/SelectionModel.cs
+++ b/src/Avalonia.Controls/SelectionModel.cs
@@ -46,17 +46,25 @@ namespace Avalonia.Controls
if (_rootNode.Source != null)
{
- if (_rootNode.Source != null)
+ // Temporarily prevent auto-select when switching source.
+ var restoreAutoSelect = _autoSelect;
+ _autoSelect = false;
+
+ try
{
using (var operation = new Operation(this))
{
ClearSelection(resetAnchor: true);
}
}
+ finally
+ {
+ _autoSelect = restoreAutoSelect;
+ }
}
_rootNode.Source = value;
- ApplyAutoSelect();
+ ApplyAutoSelect(true);
RaisePropertyChanged("Source");
@@ -114,7 +122,7 @@ namespace Avalonia.Controls
if (_autoSelect != value)
{
_autoSelect = value;
- ApplyAutoSelect();
+ ApplyAutoSelect(true);
}
}
}
@@ -133,7 +141,7 @@ namespace Avalonia.Controls
while (current?.AnchorIndex >= 0)
{
path.Add(current.AnchorIndex);
- current = current.GetAt(current.AnchorIndex, false);
+ current = current.GetAt(current.AnchorIndex, false, default);
}
anchor = new IndexPath(path);
@@ -188,7 +196,6 @@ namespace Avalonia.Controls
using var operation = new Operation(this);
ClearSelection(resetAnchor: true);
SelectWithPathImpl(value, select: true);
- ApplyAutoSelect();
}
}
}
@@ -384,21 +391,18 @@ namespace Avalonia.Controls
{
using var operation = new Operation(this);
SelectImpl(index, select: false);
- ApplyAutoSelect();
}
public void Deselect(int groupIndex, int itemIndex)
{
using var operation = new Operation(this);
SelectWithGroupImpl(groupIndex, itemIndex, select: false);
- ApplyAutoSelect();
}
public void DeselectAt(IndexPath index)
{
using var operation = new Operation(this);
SelectWithPathImpl(index, select: false);
- ApplyAutoSelect();
}
public bool IsSelected(int index) => _rootNode.IsSelected(index);
@@ -416,7 +420,7 @@ namespace Avalonia.Controls
for (int i = 0; i < path.GetSize() - 1; i++)
{
var childIndex = path.GetAt(i);
- node = node.GetAt(childIndex, realizeChild: false);
+ node = node.GetAt(childIndex, false, default);
if (node == null)
{
@@ -451,7 +455,7 @@ namespace Avalonia.Controls
}
var isSelected = (bool?)false;
- var childNode = _rootNode.GetAt(groupIndex, realizeChild: false);
+ var childNode = _rootNode.GetAt(groupIndex, false, default);
if (childNode != null)
{
@@ -470,7 +474,7 @@ namespace Avalonia.Controls
for (int i = 0; i < path.GetSize() - 1; i++)
{
var childIndex = path.GetAt(i);
- node = node.GetAt(childIndex, realizeChild: false);
+ node = node.GetAt(childIndex, false, default);
if (node == null)
{
@@ -565,7 +569,6 @@ namespace Avalonia.Controls
{
using var operation = new Operation(this);
ClearSelection(resetAnchor: true);
- ApplyAutoSelect();
}
public IDisposable Update() => new Operation(this);
@@ -592,10 +595,13 @@ namespace Avalonia.Controls
}
OnSelectionChanged(e);
- ApplyAutoSelect();
+ ApplyAutoSelect(true);
}
- internal IObservable? ResolvePath(object data, IndexPath dataIndexPath)
+ internal IObservable? ResolvePath(
+ object data,
+ IndexPath dataIndexPath,
+ IndexPath finalIndexPath)
{
IObservable? resolved = null;
@@ -604,18 +610,22 @@ namespace Avalonia.Controls
{
if (_childrenRequestedEventArgs == null)
{
- _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(data, dataIndexPath, false);
+ _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(
+ data,
+ dataIndexPath,
+ finalIndexPath,
+ false);
}
else
{
- _childrenRequestedEventArgs.Initialize(data, dataIndexPath, false);
+ _childrenRequestedEventArgs.Initialize(data, dataIndexPath, finalIndexPath, false);
}
ChildrenRequested(this, _childrenRequestedEventArgs);
resolved = _childrenRequestedEventArgs.Children;
// Clear out the values in the args so that it cannot be used after the event handler call.
- _childrenRequestedEventArgs.Initialize(null, default, true);
+ _childrenRequestedEventArgs.Initialize(null, default, default, true);
}
return resolved;
@@ -632,6 +642,8 @@ namespace Avalonia.Controls
{
AnchorIndex = default;
}
+
+ OnSelectionChanged();
}
private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null)
@@ -667,6 +679,8 @@ namespace Avalonia.Controls
{
AnchorIndex = new IndexPath(index);
}
+
+ OnSelectionChanged();
}
private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select)
@@ -676,13 +690,15 @@ namespace Avalonia.Controls
ClearSelection(resetAnchor: true);
}
- var childNode = _rootNode.GetAt(groupIndex, realizeChild: true);
+ var childNode = _rootNode.GetAt(groupIndex, true, new IndexPath(groupIndex, itemIndex));
var selected = childNode!.Select(itemIndex, select);
if (selected)
{
AnchorIndex = new IndexPath(groupIndex, itemIndex);
}
+
+ OnSelectionChanged();
}
private void SelectWithPathImpl(IndexPath index, bool select)
@@ -711,6 +727,8 @@ namespace Avalonia.Controls
{
AnchorIndex = index;
}
+
+ OnSelectionChanged();
}
private void SelectRangeFromAnchorImpl(int index, bool select)
@@ -724,6 +742,7 @@ namespace Avalonia.Controls
}
_rootNode.SelectRange(new IndexRange(anchorIndex, index), select);
+ OnSelectionChanged();
}
private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select)
@@ -752,11 +771,13 @@ namespace Avalonia.Controls
for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++)
{
- var groupNode = _rootNode.GetAt(groupIdx, realizeChild: true)!;
+ var groupNode = _rootNode.GetAt(groupIdx, true, new IndexPath(endGroupIndex, endItemIndex))!;
int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0;
int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1;
groupNode.SelectRange(new IndexRange(startIndex, endIndex), select);
}
+
+ OnSelectionChanged();
}
private void SelectRangeImpl(IndexPath start, IndexPath end, bool select)
@@ -784,6 +805,8 @@ namespace Avalonia.Controls
info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select);
}
});
+
+ OnSelectionChanged();
}
private void BeginOperation()
@@ -806,6 +829,8 @@ namespace Avalonia.Controls
if (--_operationCount == 0)
{
+ ApplyAutoSelect(false);
+
var changes = new List();
_rootNode.EndOperation(changes);
@@ -827,7 +852,7 @@ namespace Avalonia.Controls
}
}
- private void ApplyAutoSelect()
+ private void ApplyAutoSelect(bool createOperation)
{
if (AutoSelect)
{
@@ -835,8 +860,15 @@ namespace Avalonia.Controls
if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0)
{
- using var operation = new Operation(this);
- SelectImpl(0, true);
+ if (createOperation)
+ {
+ using var operation = new Operation(this);
+ SelectImpl(0, true);
+ }
+ else
+ {
+ SelectImpl(0, true);
+ }
}
}
}
diff --git a/src/Avalonia.Controls/SelectionModelChangeSet.cs b/src/Avalonia.Controls/SelectionModelChangeSet.cs
index 6e77dc5755..d1df38656a 100644
--- a/src/Avalonia.Controls/SelectionModelChangeSet.cs
+++ b/src/Avalonia.Controls/SelectionModelChangeSet.cs
@@ -135,7 +135,7 @@ namespace Avalonia.Controls
if (index >= currentIndex && index < currentIndex + currentCount)
{
int targetIndex = GetIndexAt(getRanges(info), index - currentIndex);
- item = info.Items?.GetAt(targetIndex);
+ item = info.Items?.Count > targetIndex ? info.Items?.GetAt(targetIndex) : null;
break;
}
diff --git a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs
index 974da0cf71..b1f3e0b2c4 100644
--- a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs
+++ b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs
@@ -16,15 +16,17 @@ namespace Avalonia.Controls
{
private object? _source;
private IndexPath _sourceIndexPath;
+ private IndexPath _finalIndexPath;
private bool _throwOnAccess;
internal SelectionModelChildrenRequestedEventArgs(
object source,
IndexPath sourceIndexPath,
+ IndexPath finalIndexPath,
bool throwOnAccess)
{
source = source ?? throw new ArgumentNullException(nameof(source));
- Initialize(source, sourceIndexPath, throwOnAccess);
+ Initialize(source, sourceIndexPath, finalIndexPath, throwOnAccess);
}
///
@@ -65,9 +67,26 @@ namespace Avalonia.Controls
}
}
+ ///
+ /// Gets the index of the final object which is being attempted to be retrieved.
+ ///
+ public IndexPath FinalIndex
+ {
+ get
+ {
+ if (_throwOnAccess)
+ {
+ throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
+ }
+
+ return _finalIndexPath;
+ }
+ }
+
internal void Initialize(
object? source,
IndexPath sourceIndexPath,
+ IndexPath finalIndexPath,
bool throwOnAccess)
{
if (!throwOnAccess && source == null)
@@ -77,6 +96,7 @@ namespace Avalonia.Controls
_source = source;
_sourceIndexPath = sourceIndexPath;
+ _finalIndexPath = finalIndexPath;
_throwOnAccess = throwOnAccess;
}
}
diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs
index 0b00db88c3..d99606673e 100644
--- a/src/Avalonia.Controls/SelectionNode.cs
+++ b/src/Avalonia.Controls/SelectionNode.cs
@@ -101,6 +101,7 @@ namespace Avalonia.Controls
ItemsSourceView = newDataSource;
+ TrimInvalidSelections();
PopulateSelectedItemsFromSelectedIndices();
HookupCollectionChangedHandler();
OnSelectionChanged();
@@ -108,6 +109,26 @@ namespace Avalonia.Controls
}
}
+ private void TrimInvalidSelections()
+ {
+ if (_selected == null || ItemsSourceView == null)
+ {
+ return;
+ }
+
+ var validRange = ItemsSourceView.Count > 0 ? new IndexRange(0, ItemsSourceView.Count - 1) : new IndexRange(-1, -1);
+ var removed = new List();
+ var removedCount = IndexRange.Intersect(_selected, validRange, removed);
+
+ if (removedCount > 0)
+ {
+ using var operation = _manager.Update();
+ SelectedCount -= removedCount;
+ OnSelectionChanged();
+ _operation!.Deselected(removed);
+ }
+ }
+
public ItemsSourceView? ItemsSourceView { get; private set; }
public int DataCount => ItemsSourceView?.Count ?? 0;
public int ChildrenNodeCount => _childrenNodes.Count;
@@ -141,7 +162,7 @@ namespace Avalonia.Controls
// create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid
// an explosion of node objects. However, I'm still creating the m_childrenNodes
// collection unfortunately.
- public SelectionNode? GetAt(int index, bool realizeChild)
+ public SelectionNode? GetAt(int index, bool realizeChild, IndexPath finalIndexPath)
{
SelectionNode? child = null;
@@ -171,7 +192,7 @@ namespace Avalonia.Controls
if (childData != null)
{
var childDataIndexPath = IndexPath.CloneWithChildIndex(index);
- resolver = _manager.ResolvePath(childData, childDataIndexPath);
+ resolver = _manager.ResolvePath(childData, childDataIndexPath, finalIndexPath);
}
if (resolver != null)
@@ -843,7 +864,7 @@ namespace Avalonia.Controls
int notSelectedCount = 0;
for (int i = 0; i < ChildrenNodeCount; i++)
{
- var child = GetAt(i, realizeChild: false);
+ var child = GetAt(i, false, default);
if (child != null)
{
diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs
index e92c8faf20..fe1a4f5ac1 100644
--- a/src/Avalonia.Controls/Slider.cs
+++ b/src/Avalonia.Controls/Slider.cs
@@ -1,12 +1,40 @@
using System;
+using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
-using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
+using Avalonia.Utilities;
namespace Avalonia.Controls
{
+
+ ///
+ /// Enum which describes how to position the ticks in a .
+ ///
+ public enum TickPlacement
+ {
+ ///
+ /// No tick marks will appear.
+ ///
+ None,
+
+ ///
+ /// Tick marks will appear above the track for a horizontal , or to the left of the track for a vertical .
+ ///
+ TopLeft,
+
+ ///
+ /// Tick marks will appear below the track for a horizontal , or to the right of the track for a vertical .
+ ///
+ BottomRight,
+
+ ///
+ /// Tick marks appear on both sides of either a horizontal or vertical .
+ ///
+ Outside
+ }
+
///
/// A control that lets the user select from a range of values by moving a Thumb control along a Track.
///
@@ -30,19 +58,31 @@ namespace Avalonia.Controls
public static readonly StyledProperty TickFrequencyProperty =
AvaloniaProperty.Register(nameof(TickFrequency), 0.0);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty TickPlacementProperty =
+ AvaloniaProperty.Register(nameof(TickPlacement), 0d);
+
// Slider required parts
+ private bool _isDragging = false;
private Track _track;
private Button _decreaseButton;
private Button _increaseButton;
+ private IDisposable _decreaseButtonPressDispose;
+ private IDisposable _decreaseButtonReleaseDispose;
+ private IDisposable _increaseButtonSubscription;
+ private IDisposable _increaseButtonReleaseDispose;
+ private IDisposable _pointerMovedDispose;
///
/// Initializes static members of the class.
///
static Slider()
{
+ PressedMixin.Attach();
OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal);
Thumb.DragStartedEvent.AddClassHandler((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble);
- Thumb.DragDeltaEvent.AddClassHandler((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble);
Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble);
}
@@ -81,57 +121,89 @@ namespace Avalonia.Controls
set { SetValue(TickFrequencyProperty, value); }
}
+ ///
+ /// Gets or sets a value that indicates where to draw
+ /// tick marks in relation to the track.
+ ///
+ public TickPlacement TickPlacement
+ {
+ get { return GetValue(TickPlacementProperty); }
+ set { SetValue(TickPlacementProperty, value); }
+ }
+
///
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
- if (_decreaseButton != null)
- {
- _decreaseButton.Click -= DecreaseClick;
- }
-
- if (_increaseButton != null)
- {
- _increaseButton.Click -= IncreaseClick;
- }
+ base.OnApplyTemplate(e);
+
+ _decreaseButtonPressDispose?.Dispose();
+ _decreaseButtonReleaseDispose?.Dispose();
+ _increaseButtonSubscription?.Dispose();
+ _increaseButtonReleaseDispose?.Dispose();
+ _pointerMovedDispose?.Dispose();
_decreaseButton = e.NameScope.Find("PART_DecreaseButton");
_track = e.NameScope.Find("PART_Track");
_increaseButton = e.NameScope.Find("PART_IncreaseButton");
+ if (_track != null)
+ {
+ _track.IsThumbDragHandled = true;
+ }
+
if (_decreaseButton != null)
{
- _decreaseButton.Click += DecreaseClick;
+ _decreaseButtonPressDispose = _decreaseButton.AddDisposableHandler(PointerPressedEvent, TrackPressed, RoutingStrategies.Tunnel);
+ _decreaseButtonReleaseDispose = _decreaseButton.AddDisposableHandler(PointerReleasedEvent, TrackReleased, RoutingStrategies.Tunnel);
}
if (_increaseButton != null)
{
- _increaseButton.Click += IncreaseClick;
+ _increaseButtonSubscription = _increaseButton.AddDisposableHandler(PointerPressedEvent, TrackPressed, RoutingStrategies.Tunnel);
+ _increaseButtonReleaseDispose = _increaseButton.AddDisposableHandler(PointerReleasedEvent, TrackReleased, RoutingStrategies.Tunnel);
}
+
+ _pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel);
}
- private void DecreaseClick(object sender, RoutedEventArgs e)
+ private void TrackMoved(object sender, PointerEventArgs e)
{
- ChangeValueBy(-LargeChange);
+ if (_isDragging)
+ {
+ MoveToPoint(e.GetCurrentPoint(_track));
+ }
}
- private void IncreaseClick(object sender, RoutedEventArgs e)
+ private void TrackReleased(object sender, PointerReleasedEventArgs e)
{
- ChangeValueBy(LargeChange);
+ _isDragging = false;
}
- private void ChangeValueBy(double by)
+ private void TrackPressed(object sender, PointerPressedEventArgs e)
{
- if (IsSnapToTickEnabled)
+ if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
- by = by < 0 ? Math.Min(-TickFrequency, by) : Math.Max(TickFrequency, by);
+ MoveToPoint(e.GetCurrentPoint(_track));
+ _isDragging = true;
}
+ }
- var value = Value;
- var next = SnapToTick(Math.Max(Math.Min(value + by, Maximum), Minimum));
- if (next != value)
- {
- Value = next;
- }
+ private void MoveToPoint(PointerPoint x)
+ {
+ var orient = Orientation == Orientation.Horizontal;
+
+ var pointDen = orient ? _track.Bounds.Width : _track.Bounds.Height;
+ // Just add epsilon to avoid NaN in case 0/0
+ pointDen += double.Epsilon;
+
+ var pointNum = orient ? x.Position.X : x.Position.Y;
+ var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d);
+ var invert = orient ? 0 : 1;
+ var calcVal = Math.Abs(invert - logicalPos);
+ var range = Maximum - Minimum;
+ var finalValue = calcVal * range + Minimum;
+
+ Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@@ -150,19 +222,7 @@ namespace Avalonia.Controls
///
protected virtual void OnThumbDragStarted(VectorEventArgs e)
{
- }
-
- ///
- /// Called when user dragging the .
- ///
- ///
- protected virtual void OnThumbDragDelta(VectorEventArgs e)
- {
- Thumb thumb = e.Source as Thumb;
- if (thumb != null && _track?.Thumb == thumb)
- {
- MoveToNextTick(_track.Value);
- }
+ _isDragging = true;
}
///
@@ -171,15 +231,7 @@ namespace Avalonia.Controls
///
protected virtual void OnThumbDragCompleted(VectorEventArgs e)
{
- }
-
- ///
- /// Searches for the closest tick and sets Value to that tick.
- ///
- /// Value that want to snap to closest Tick.
- private void MoveToNextTick(double value)
- {
- Value = SnapToTick(Math.Max(Minimum, Math.Min(Maximum, value)));
+ _isDragging = false;
}
///
@@ -188,14 +240,17 @@ namespace Avalonia.Controls
/// Value that want to snap to closest Tick.
private double SnapToTick(double value)
{
- if (IsSnapToTickEnabled && TickFrequency > 0.0)
+ var previous = Minimum;
+ var next = Maximum;
+
+ if (TickFrequency > 0.0)
{
- double previous = Minimum + (Math.Round(((value - Minimum) / TickFrequency)) * TickFrequency);
- double next = Math.Min(Maximum, previous + TickFrequency);
- value = value > (previous + next) * 0.5 ? next : previous;
+ previous = Minimum + (Math.Round((value - Minimum) / TickFrequency) * TickFrequency);
+ next = Math.Min(Maximum, previous + TickFrequency);
}
- return value;
+ // Choose the closest value between previous and next. If tie, snap to 'next'.
+ return MathUtilities.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous;
}
private void UpdatePseudoClasses(Orientation o)
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 42e16e6979..394699ce64 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -347,7 +347,7 @@ namespace Avalonia.Controls
if (IsFocused)
{
- DecideCaretVisibility();
+ _presenter?.ShowCaret();
}
}
@@ -364,14 +364,7 @@ namespace Avalonia.Controls
{
SelectAll();
}
- else
- {
- DecideCaretVisibility();
- }
- }
- private void DecideCaretVisibility()
- {
_presenter?.ShowCaret();
}
@@ -580,15 +573,15 @@ namespace Avalonia.Controls
switch (e.Key)
{
case Key.Left:
- MoveHorizontal(-1, hasWholeWordModifiers);
- movement = true;
selection = DetectSelection();
+ MoveHorizontal(-1, hasWholeWordModifiers, selection);
+ movement = true;
break;
case Key.Right:
- MoveHorizontal(1, hasWholeWordModifiers);
- movement = true;
selection = DetectSelection();
+ MoveHorizontal(1, hasWholeWordModifiers, selection);
+ movement = true;
break;
case Key.Up:
@@ -833,13 +826,21 @@ namespace Avalonia.Controls
return result;
}
- private void MoveHorizontal(int direction, bool wholeWord)
+ private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting)
{
var text = Text ?? string.Empty;
var caretIndex = CaretIndex;
if (!wholeWord)
{
+ if (SelectionStart != SelectionEnd && !isSelecting)
+ {
+ var start = Math.Min(SelectionStart, SelectionEnd);
+ var end = Math.Max(SelectionStart, SelectionEnd);
+ CaretIndex = direction < 0 ? start : end;
+ return;
+ }
+
var index = caretIndex + direction;
if (index < 0 || index > text.Length)
@@ -975,6 +976,7 @@ namespace Avalonia.Controls
{
SelectionStart = 0;
SelectionEnd = Text?.Length ?? 0;
+ CaretIndex = SelectionEnd;
}
private bool DeleteSelection()
@@ -1055,14 +1057,14 @@ namespace Avalonia.Controls
private void SetSelectionForControlBackspace()
{
SelectionStart = CaretIndex;
- MoveHorizontal(-1, true);
+ MoveHorizontal(-1, true, false);
SelectionEnd = CaretIndex;
}
private void SetSelectionForControlDelete()
{
SelectionStart = CaretIndex;
- MoveHorizontal(1, true);
+ MoveHorizontal(1, true, false);
SelectionEnd = CaretIndex;
}
diff --git a/src/Avalonia.Controls/TickBar.cs b/src/Avalonia.Controls/TickBar.cs
new file mode 100644
index 0000000000..16e063beb3
--- /dev/null
+++ b/src/Avalonia.Controls/TickBar.cs
@@ -0,0 +1,416 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Enum which describes how to position the TickBar.
+ ///
+ public enum TickBarPlacement
+ {
+ ///
+ /// Position this tick at the left of target element.
+ ///
+ Left,
+
+ ///
+ /// Position this tick at the top of target element.
+ ///
+ Top,
+
+ ///
+ /// Position this tick at the right of target element.
+ ///
+ Right,
+
+ ///
+ /// Position this tick at the bottom of target element.
+ ///
+ Bottom,
+ }
+
+
+ ///
+ /// An element that is used for drawing 's Ticks.
+ ///
+ public class TickBar : Control
+ {
+ static TickBar()
+ {
+ AffectsRender(ReservedSpaceProperty,
+ MaximumProperty,
+ MinimumProperty,
+ OrientationProperty,
+ TickFrequencyProperty);
+ }
+
+ public TickBar() : base()
+ {
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty FillProperty =
+ AvaloniaProperty.Register(nameof(Fill));
+
+ ///
+ /// Brush used to fill the TickBar's Ticks.
+ ///
+ public IBrush Fill
+ {
+ get { return GetValue(FillProperty); }
+ set { SetValue(FillProperty, value); }
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinimumProperty =
+ AvaloniaProperty.Register(nameof(Minimum), 0d);
+
+ ///
+ /// Logical position where the Minimum Tick will be drawn
+ ///
+ public double Minimum
+ {
+ get { return GetValue(MinimumProperty); }
+ set
+ {
+ SetValue(MinimumProperty, value);
+ }
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaximumProperty =
+ AvaloniaProperty.Register(nameof(Maximum), 0d);
+
+ ///
+ /// Logical position where the Maximum Tick will be drawn
+ ///
+ public double Maximum
+ {
+ get { return GetValue(MaximumProperty); }
+ set { SetValue(MaximumProperty, value); }
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty TickFrequencyProperty =
+ AvaloniaProperty.Register(nameof(TickFrequency), 0d);
+
+ ///
+ /// TickFrequency property defines how the tick will be drawn.
+ ///
+ public double TickFrequency
+ {
+ get { return GetValue(TickFrequencyProperty); }
+ set { SetValue(TickFrequencyProperty, value); }
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty OrientationProperty =
+ AvaloniaProperty.Register(nameof(Orientation));
+
+ ///
+ /// TickBar parent's orientation.
+ ///
+ public Orientation Orientation
+ {
+ get { return GetValue(OrientationProperty); }
+ set { SetValue(OrientationProperty, value); }
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty> TicksProperty =
+ AvaloniaProperty.Register>(nameof(Ticks));
+
+ ///
+ /// The Ticks property contains collection of value of type Double which
+ /// are the logical positions use to draw the ticks.
+ /// The property value is a .
+ ///
+ public List Ticks
+ {
+ get { return GetValue(TicksProperty); }
+ set { SetValue(TicksProperty, value); }
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsDirectionReversedProperty =
+ AvaloniaProperty.Register(nameof(IsDirectionReversed), false);
+
+ ///
+ /// The IsDirectionReversed property defines the direction of value incrementation.
+ /// By default, if Tick's orientation is Horizontal, ticks will be drawn from left to right.
+ /// (And, bottom to top for Vertical orientation).
+ /// If IsDirectionReversed is 'true' the direction of the drawing will be in opposite direction.
+ ///
+ public bool IsDirectionReversed
+ {
+ get { return GetValue(IsDirectionReversedProperty); }
+ set { SetValue(IsDirectionReversedProperty, value); }
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementProperty =
+ AvaloniaProperty.Register(nameof(Placement), 0d);
+
+
+ ///
+ /// Placement property specified how the Tick will be placed.
+ /// This property affects the way ticks are drawn.
+ /// This property has type of .
+ ///
+ public TickBarPlacement Placement
+ {
+ get { return GetValue(PlacementProperty); }
+ set { SetValue(PlacementProperty, value); }
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ReservedSpaceProperty =
+ AvaloniaProperty.Register(nameof(ReservedSpace));
+
+ ///
+ /// TickBar will use ReservedSpaceProperty for left and right spacing (for horizontal orientation) or
+ /// tob and bottom spacing (for vertical orienation).
+ /// The space on both sides of TickBar is half of specified ReservedSpace.
+ /// This property has type of .
+ ///
+ public Rect ReservedSpace
+ {
+ get { return GetValue(ReservedSpaceProperty); }
+ set { SetValue(ReservedSpaceProperty, value); }
+ }
+
+ ///
+ /// Draw ticks.
+ /// Ticks can be draw in 8 diffrent ways depends on Placment property and IsDirectionReversed property.
+ ///
+ /// This function also draw selection-tick(s) if IsSelectionRangeEnabled is 'true' and
+ /// SelectionStart and SelectionEnd are valid.
+ ///
+ /// The primary ticks (for Mininum and Maximum value) height will be 100% of TickBar's render size (use Width or Height
+ /// depends on Placement property).
+ ///
+ /// The secondary ticks (all other ticks, including selection-tics) height will be 75% of TickBar's render size.
+ ///
+ /// Brush that use to fill ticks is specified by Shape.Fill property.
+ ///
+ /// Pen that use to draw ticks is specified by Shape.Pen property.
+ ///
+ public override void Render(DrawingContext dc)
+ {
+ var size = new Size(Bounds.Width, Bounds.Height);
+ var range = Maximum - Minimum;
+ var tickLen = 0.0d; // Height for Primary Tick (for Mininum and Maximum value)
+ var tickLen2 = 0.0d; // Height for Secondary Tick
+ var logicalToPhysical = 1.0;
+ var progression = 1.0d;
+ var startPoint = new Point();
+ var endPoint = new Point();
+ var rSpace = Orientation == Orientation.Horizontal ? ReservedSpace.Width : ReservedSpace.Height;
+
+ // Take Thumb size in to account
+ double halfReservedSpace = rSpace * 0.5;
+
+ switch (Placement)
+ {
+ case TickBarPlacement.Top:
+ if (MathUtilities.GreaterThanOrClose(rSpace, size.Width))
+ {
+ return;
+ }
+ size = new Size(size.Width - rSpace, size.Height);
+ tickLen = -size.Height;
+ startPoint = new Point(halfReservedSpace, size.Height);
+ endPoint = new Point(halfReservedSpace + size.Width, size.Height);
+ logicalToPhysical = size.Width / range;
+ progression = 1;
+ break;
+
+ case TickBarPlacement.Bottom:
+ if (MathUtilities.GreaterThanOrClose(rSpace, size.Width))
+ {
+ return;
+ }
+ size = new Size(size.Width - rSpace, size.Height);
+ tickLen = size.Height;
+ startPoint = new Point(halfReservedSpace, 0d);
+ endPoint = new Point(halfReservedSpace + size.Width, 0d);
+ logicalToPhysical = size.Width / range;
+ progression = 1;
+ break;
+
+ case TickBarPlacement.Left:
+ if (MathUtilities.GreaterThanOrClose(rSpace, size.Height))
+ {
+ return;
+ }
+ size = new Size(size.Width, size.Height - rSpace);
+
+ tickLen = -size.Width;
+ startPoint = new Point(size.Width, size.Height + halfReservedSpace);
+ endPoint = new Point(size.Width, halfReservedSpace);
+ logicalToPhysical = size.Height / range * -1;
+ progression = -1;
+ break;
+
+ case TickBarPlacement.Right:
+ if (MathUtilities.GreaterThanOrClose(rSpace, size.Height))
+ {
+ return;
+ }
+ size = new Size(size.Width, size.Height - rSpace);
+ tickLen = size.Width;
+ startPoint = new Point(0d, size.Height + halfReservedSpace);
+ endPoint = new Point(0d, halfReservedSpace);
+ logicalToPhysical = size.Height / range * -1;
+ progression = -1;
+ break;
+ };
+
+ tickLen2 = tickLen * 0.75;
+
+ // Invert direciton of the ticks
+ if (IsDirectionReversed)
+ {
+ progression *= -progression;
+ logicalToPhysical *= -1;
+
+ // swap startPoint & endPoint
+ var pt = startPoint;
+ startPoint = endPoint;
+ endPoint = pt;
+ }
+
+ var pen = new Pen(Fill, 1.0d);
+
+ // Is it Vertical?
+ if (Placement == TickBarPlacement.Left || Placement == TickBarPlacement.Right)
+ {
+ // Reduce tick interval if it is more than would be visible on the screen
+ double interval = TickFrequency;
+ if (interval > 0.0)
+ {
+ double minInterval = (Maximum - Minimum) / size.Height;
+ if (interval < minInterval)
+ {
+ interval = minInterval;
+ }
+ }
+
+ // Draw Min & Max tick
+ dc.DrawLine(pen, startPoint, new Point(startPoint.X + tickLen, startPoint.Y));
+ dc.DrawLine(pen, new Point(startPoint.X, endPoint.Y),
+ new Point(startPoint.X + tickLen, endPoint.Y));
+
+ // This property is rarely set so let's try to avoid the GetValue
+ // caching of the mutable default value
+ var ticks = Ticks ?? null;
+
+ // Draw ticks using specified Ticks collection
+ if (ticks?.Count > 0)
+ {
+ for (int i = 0; i < ticks.Count; i++)
+ {
+ if (MathUtilities.LessThanOrClose(ticks[i], Minimum) || MathUtilities.GreaterThanOrClose(ticks[i], Maximum))
+ {
+ continue;
+ }
+
+ double adjustedTick = ticks[i] - Minimum;
+
+ double y = adjustedTick * logicalToPhysical + startPoint.Y;
+ dc.DrawLine(pen,
+ new Point(startPoint.X, y),
+ new Point(startPoint.X + tickLen2, y));
+ }
+ }
+ // Draw ticks using specified TickFrequency
+ else if (interval > 0.0)
+ {
+ for (double i = interval; i < range; i += interval)
+ {
+ double y = i * logicalToPhysical + startPoint.Y;
+
+ dc.DrawLine(pen,
+ new Point(startPoint.X, y),
+ new Point(startPoint.X + tickLen2, y));
+ }
+ }
+ }
+ else // Placement == Top || Placement == Bottom
+ {
+ // Reduce tick interval if it is more than would be visible on the screen
+ double interval = TickFrequency;
+ if (interval > 0.0)
+ {
+ double minInterval = (Maximum - Minimum) / size.Width;
+ if (interval < minInterval)
+ {
+ interval = minInterval;
+ }
+ }
+
+ // Draw Min & Max tick
+ dc.DrawLine(pen, startPoint, new Point(startPoint.X, startPoint.Y + tickLen));
+ dc.DrawLine(pen, new Point(endPoint.X, startPoint.Y),
+ new Point(endPoint.X, startPoint.Y + tickLen));
+
+ // This property is rarely set so let's try to avoid the GetValue
+ // caching of the mutable default value
+ var ticks = Ticks ?? null;
+
+ // Draw ticks using specified Ticks collection
+ if (ticks?.Count > 0)
+ {
+ for (int i = 0; i < ticks.Count; i++)
+ {
+ if (MathUtilities.LessThanOrClose(ticks[i], Minimum) || MathUtilities.GreaterThanOrClose(ticks[i], Maximum))
+ {
+ continue;
+ }
+ double adjustedTick = ticks[i] - Minimum;
+
+ double x = adjustedTick * logicalToPhysical + startPoint.X;
+ dc.DrawLine(pen,
+ new Point(x, startPoint.Y),
+ new Point(x, startPoint.Y + tickLen2));
+ }
+ }
+ // Draw ticks using specified TickFrequency
+ else if (interval > 0.0)
+ {
+ for (double i = interval; i < range; i += interval)
+ {
+ double x = i * logicalToPhysical + startPoint.X;
+ dc.DrawLine(pen,
+ new Point(x, startPoint.Y),
+ new Point(x, startPoint.Y + tickLen2));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs
new file mode 100644
index 0000000000..6c6426a31d
--- /dev/null
+++ b/src/Avalonia.Controls/ToggleSwitch.cs
@@ -0,0 +1,105 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.LogicalTree;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// A Toggle Switch control.
+ ///
+ public class ToggleSwitch : ToggleButton
+ {
+ static ToggleSwitch()
+ {
+ OffContentProperty.Changed.AddClassHandler((x, e) => x.OffContentChanged(e));
+ OnContentProperty.Changed.AddClassHandler((x, e) => x.OnContentChanged(e));
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty OffContentProperty =
+ AvaloniaProperty.Register(nameof(OffContent), defaultValue: "Off");
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty OffContentTemplateProperty =
+ AvaloniaProperty.Register(nameof(OffContentTemplate));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty OnContentProperty =
+ AvaloniaProperty.Register(nameof(OnContent), defaultValue: "On");
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty OnContentTemplateProperty =
+ AvaloniaProperty.Register(nameof(OnContentTemplate));
+
+ ///
+ /// Gets or Sets the Content that is displayed when in the On State.
+ ///
+ public object OnContent
+ {
+ get { return GetValue(OnContentProperty); }
+ set { SetValue(OnContentProperty, value); }
+ }
+
+ ///
+ /// Gets or Sets the Content that is displayed when in the Off State.
+ ///
+ public object OffContent
+ {
+ get { return GetValue(OffContentProperty); }
+ set { SetValue(OffContentProperty, value); }
+ }
+
+ ///
+ /// Gets or Sets the used to display the .
+ ///
+ public IDataTemplate OffContentTemplate
+ {
+ get { return GetValue(OffContentTemplateProperty); }
+ set { SetValue(OffContentTemplateProperty, value); }
+ }
+
+ ///
+ /// Gets or Sets the used to display the .
+ ///
+ public IDataTemplate OnContentTemplate
+ {
+ get { return GetValue(OnContentTemplateProperty); }
+ set { SetValue(OnContentTemplateProperty, value); }
+ }
+
+ private void OffContentChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.OldValue is ILogical oldChild)
+ {
+ LogicalChildren.Remove(oldChild);
+ }
+
+ if (e.NewValue is ILogical newChild)
+ {
+ LogicalChildren.Add(newChild);
+ }
+ }
+
+ private void OnContentChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.OldValue is ILogical oldChild)
+ {
+ LogicalChildren.Remove(oldChild);
+ }
+
+ if (e.NewValue is ILogical newChild)
+ {
+ LogicalChildren.Add(newChild);
+ }
+ }
+ }
+}
+
diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs
index 2f11746891..b458b15c64 100644
--- a/src/Avalonia.Controls/ToolTip.cs
+++ b/src/Avalonia.Controls/ToolTip.cs
@@ -204,13 +204,15 @@ namespace Avalonia.Controls
private static void IsOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
+ var newValue = (bool)e.NewValue;
+ ToolTip toolTip;
- if ((bool)e.NewValue)
+ if (newValue)
{
var tip = GetTip(control);
if (tip == null) return;
- var toolTip = control.GetValue(ToolTipProperty);
+ toolTip = control.GetValue(ToolTipProperty);
if (toolTip == null || (tip != toolTip && tip != toolTip.Content))
{
toolTip?.Close();
@@ -223,9 +225,11 @@ namespace Avalonia.Controls
}
else
{
- var toolTip = control.GetValue(ToolTipProperty);
+ toolTip = control.GetValue(ToolTipProperty);
toolTip?.Close();
}
+
+ toolTip?.UpdatePseudoClasses(newValue);
}
private void Open(Control control)
@@ -238,6 +242,9 @@ namespace Avalonia.Controls
_popup.ConfigurePosition(control, GetPlacement(control),
new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
+
+ WindowManagerAddShadowHintChanged(_popup, false);
+
_popup.Show();
}
@@ -250,5 +257,18 @@ namespace Avalonia.Controls
_popup = null;
}
}
+
+ private void WindowManagerAddShadowHintChanged(IPopupHost host, bool hint)
+ {
+ if (host is PopupRoot pr)
+ {
+ pr.PlatformImpl.SetWindowManagerAddShadowHint(hint);
+ }
+ }
+
+ private void UpdatePseudoClasses(bool newValue)
+ {
+ PseudoClasses.Set(":open", newValue);
+ }
}
}
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 8335e03487..e431076d2c 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -276,9 +276,6 @@ namespace Avalonia.Controls
set { SetValue(AccessText.ShowAccessKeyProperty, value); }
}
- ///
- Size ILayoutRoot.MaxClientSize => Size.Infinity;
-
///
double ILayoutRoot.LayoutScaling => PlatformImpl?.Scaling ?? 1;
diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs
index 95e7437838..a91655855c 100644
--- a/src/Avalonia.Controls/TreeView.cs
+++ b/src/Avalonia.Controls/TreeView.cs
@@ -119,9 +119,10 @@ namespace Avalonia.Controls
///
/// Gets or sets the selected item.
///
- ///
- /// Gets or sets the selected item.
- ///
+ ///
+ /// Note that setting this property only currently works if the item is expanded to be visible.
+ /// To select non-expanded nodes use `Selection.SelectedIndex`.
+ ///
public object SelectedItem
{
get => Selection.SelectedItem;
@@ -346,7 +347,7 @@ namespace Avalonia.Controls
if (container != null)
{
- container.BringIntoView();
+ DispatcherTimer.RunOnce(container.BringIntoView, TimeSpan.Zero);
}
}
}
@@ -395,10 +396,17 @@ namespace Avalonia.Controls
private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e)
{
- var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as ItemsControl;
+ var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as TreeViewItem;
if (container is object)
{
+ if (e.SourceIndex.IsAncestorOf(e.FinalIndex))
+ {
+ container.IsExpanded = true;
+ container.ApplyTemplate();
+ container.Presenter?.ApplyTemplate();
+ }
+
e.Children = Observable.CombineLatest(
container.GetObservable(TreeViewItem.IsExpandedProperty),
container.GetObservable(ItemsProperty),
diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs
index 51eb6edbea..438cbc8b27 100644
--- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs
+++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs
@@ -1,6 +1,7 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
+using Avalonia.Utilities;
namespace Avalonia.Controls.Utils
{
@@ -119,7 +120,7 @@ namespace Avalonia.Controls.Utils
}
var rect = new Rect(_size);
- if (Math.Abs(borderThickness) > double.Epsilon)
+ if (!MathUtilities.IsZero(borderThickness))
rect = rect.Deflate(borderThickness * 0.5);
var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight,
_cornerRadius.BottomRight, _cornerRadius.BottomLeft);
diff --git a/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs b/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs
index 430ecabbb8..5adf5bdeea 100644
--- a/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs
+++ b/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs
@@ -28,7 +28,7 @@ namespace Avalonia.Controls.Utils
if (depth < path.GetSize() - 1)
{
- node = node.GetAt(childIndex, realizeChildren)!;
+ node = node.GetAt(childIndex, realizeChildren, path)!;
}
}
}
@@ -50,7 +50,7 @@ namespace Avalonia.Controls.Utils
int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount;
for (int i = count - 1; i >= 0; i--)
{
- var child = nextNode.Node.GetAt(i, realizeChildren);
+ var child = nextNode.Node.GetAt(i, realizeChildren, nextNode.Path);
var childPath = nextNode.Path.CloneWithChildIndex(i);
if (child != null)
{
@@ -90,7 +90,7 @@ namespace Avalonia.Controls.Utils
for (int i = endIndex; i >= startIndex; i--)
{
- var child = node.GetAt(i, realizeChild: true);
+ var child = node.GetAt(i, true, end);
if (child != null)
{
var childPath = currentPath.CloneWithChildIndex(i);
@@ -112,7 +112,7 @@ namespace Avalonia.Controls.Utils
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1;
for (int i = endIndex; i >= startIndex; i--)
{
- var child = info.Node.GetAt(i, realizeChild: true);
+ var child = info.Node.GetAt(i, true, end);
if (child != null)
{
var childPath = info.Path.CloneWithChildIndex(i);
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index 474d845905..ff7cc41e3b 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -69,7 +69,7 @@ namespace Avalonia.Controls
///
public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot
{
- private List _children = new List();
+ private readonly List<(Window child, bool isDialog)> _children = new List<(Window, bool)>();
///
/// Defines the property.
@@ -188,7 +188,7 @@ namespace Avalonia.Controls
impl.Closing = HandleClosing;
impl.GotInputWhenDisabled = OnGotInputWhenDisabled;
impl.WindowStateChanged = HandleWindowStateChanged;
- _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
+ _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x));
PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar);
@@ -318,9 +318,6 @@ namespace Avalonia.Controls
///
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e);
- ///
- Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;
-
///
Type IStyleable.StyleKey => typeof(Window);
@@ -376,7 +373,7 @@ namespace Avalonia.Controls
private void CloseInternal()
{
- foreach (var child in _children.ToList())
+ foreach (var (child, _) in _children.ToList())
{
// if we HandleClosing() before then there will be no children.
child.CloseInternal();
@@ -399,7 +396,7 @@ namespace Avalonia.Controls
{
bool canClose = true;
- foreach (var child in _children.ToList())
+ foreach (var (child, _) in _children.ToList())
{
if (!child.HandleClosing())
{
@@ -472,6 +469,28 @@ namespace Avalonia.Controls
/// The window has already been closed.
///
public override void Show()
+ {
+ ShowCore(null);
+ }
+
+ ///
+ /// Shows the window as a child of .
+ ///
+ /// Window that will be a parent of the shown window.
+ ///
+ /// The window has already been closed.
+ ///
+ public void Show(Window parent)
+ {
+ if (parent is null)
+ {
+ throw new ArgumentNullException(nameof(parent), "Showing a child window requires valid parent.");
+ }
+
+ ShowCore(parent);
+ }
+
+ private void ShowCore(Window parent)
{
if (PlatformImpl == null)
{
@@ -483,7 +502,7 @@ namespace Avalonia.Controls
return;
}
- this.RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
+ RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
EnsureInitialized();
IsVisible = true;
@@ -504,6 +523,14 @@ namespace Avalonia.Controls
using (BeginAutoSizing())
{
+ if (parent != null)
+ {
+ PlatformImpl?.SetParent(parent.PlatformImpl);
+ }
+
+ Owner = parent;
+ parent?.AddChild(this, false);
+
PlatformImpl?.Show();
Renderer?.Start();
}
@@ -571,9 +598,9 @@ namespace Avalonia.Controls
using (BeginAutoSizing())
{
- PlatformImpl.SetParent(owner.PlatformImpl);
+ PlatformImpl?.SetParent(owner.PlatformImpl);
Owner = owner;
- owner.AddChild(this);
+ owner.AddChild(this, true);
PlatformImpl?.Show();
Renderer?.Start();
@@ -598,28 +625,57 @@ namespace Avalonia.Controls
private void UpdateEnabled()
{
- PlatformImpl.SetEnabled(_children.Count == 0);
+ bool isEnabled = true;
+
+ foreach (var (_, isDialog) in _children)
+ {
+ if (isDialog)
+ {
+ isEnabled = false;
+ break;
+ }
+ }
+
+ PlatformImpl.SetEnabled(isEnabled);
}
- private void AddChild(Window window)
+ private void AddChild(Window window, bool isDialog)
{
- _children.Add(window);
+ _children.Add((window, isDialog));
UpdateEnabled();
}
private void RemoveChild(Window window)
{
- _children.Remove(window);
+ for (int i = _children.Count - 1; i >= 0; i--)
+ {
+ var (child, _) = _children[i];
+
+ if (ReferenceEquals(child, window))
+ {
+ _children.RemoveAt(i);
+ }
+ }
+
UpdateEnabled();
}
private void OnGotInputWhenDisabled()
{
- var firstChild = _children.FirstOrDefault();
+ Window firstDialogChild = null;
+
+ foreach (var (child, isDialog) in _children)
+ {
+ if (isDialog)
+ {
+ firstDialogChild = child;
+ break;
+ }
+ }
- if (firstChild != null)
+ if (firstDialogChild != null)
{
- firstChild.OnGotInputWhenDisabled();
+ firstDialogChild.OnGotInputWhenDisabled();
}
else
{
@@ -663,15 +719,16 @@ namespace Avalonia.Controls
var sizeToContent = SizeToContent;
var clientSize = ClientSize;
var constraint = clientSize;
+ var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity;
if (sizeToContent.HasFlagCustom(SizeToContent.Width))
{
- constraint = constraint.WithWidth(double.PositiveInfinity);
+ constraint = constraint.WithWidth(maxAutoSize.Width);
}
if (sizeToContent.HasFlagCustom(SizeToContent.Height))
{
- constraint = constraint.WithHeight(double.PositiveInfinity);
+ constraint = constraint.WithHeight(maxAutoSize.Height);
}
var result = base.MeasureOverride(constraint);
diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
index 844489ef97..cd64af60e2 100644
--- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
+++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
@@ -44,7 +44,7 @@ namespace Avalonia.DesignerSupport.Remote
public IPlatformHandle Handle { get; }
public WindowState WindowState { get; set; }
public Action WindowStateChanged { get; set; }
- public Size MaxClientSize { get; } = new Size(4096, 4096);
+ public Size MaxAutoSizeHint { get; } = new Size(4096, 4096);
public event Action LostFocus
{
add {}
diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs
index 64b3af4ea2..b001bc1b76 100644
--- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs
+++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs
@@ -19,7 +19,7 @@ namespace Avalonia.DesignerSupport.Remote
public Action Deactivated { get; set; }
public Action Activated { get; set; }
public IPlatformHandle Handle { get; }
- public Size MaxClientSize { get; }
+ public Size MaxAutoSizeHint { get; }
public Size ClientSize { get; }
public double Scaling { get; } = 1.0;
public IEnumerable Surfaces { get; }
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
index 7addaba977..1d19e1a346 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
@@ -2,6 +2,7 @@
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
+using Avalonia.Threading;
namespace Avalonia.Diagnostics.ViewModels
{
@@ -49,7 +50,21 @@ namespace Avalonia.Diagnostics.ViewModels
value is TreePageViewModel newTree &&
oldTree?.SelectedNode?.Visual is IControl control)
{
- newTree.SelectControl(control);
+ // HACK: We want to select the currently selected control in the new tree, but
+ // to select nested nodes in TreeView, currently the TreeView has to be able to
+ // expand the parent nodes. Because at this point the TreeView isn't visible,
+ // this will fail unless we schedule the selection to run after layout.
+ DispatcherTimer.RunOnce(
+ () =>
+ {
+ try
+ {
+ newTree.SelectControl(control);
+ }
+ catch { }
+ },
+ TimeSpan.FromMilliseconds(0),
+ DispatcherPriority.ApplicationIdle);
}
RaiseAndSetIfChanged(ref _content, value);
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
index c8a9da600d..aa27538abc 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
@@ -1,11 +1,11 @@
using System;
+using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.LogicalTree;
-using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
@@ -82,5 +82,41 @@ namespace Avalonia.Diagnostics.ViewModels
get;
private set;
}
+
+ public IndexPath Index
+ {
+ get
+ {
+ var indices = new List();
+ var child = this;
+ var parent = Parent;
+
+ while (parent is object)
+ {
+ indices.Add(IndexOf(parent.Children, child));
+ child = child.Parent;
+ parent = parent.Parent;
+ }
+
+ indices.Add(0);
+ indices.Reverse();
+ return new IndexPath(indices);
+ }
+ }
+
+ private static int IndexOf(IReadOnlyList collection, TreeNode item)
+ {
+ var count = collection.Count;
+
+ for (var i = 0; i < count; ++i)
+ {
+ if (collection[i] == item)
+ {
+ return i;
+ }
+ }
+
+ throw new AvaloniaInternalException("TreeNode was not present in parent Children collection.");
+ }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
index 26b5fe2524..38ac88a83c 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
@@ -6,28 +6,40 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class TreePageViewModel : ViewModelBase, IDisposable
{
- private TreeNode _selected;
+ private TreeNode _selectedNode;
private ControlDetailsViewModel _details;
private string _propertyFilter;
public TreePageViewModel(TreeNode[] nodes)
{
Nodes = nodes;
- }
+ Selection = new SelectionModel
+ {
+ SingleSelect = true,
+ Source = Nodes
+ };
+
+ Selection.SelectionChanged += (s, e) =>
+ {
+ SelectedNode = (TreeNode)Selection.SelectedItem;
+ };
+ }
public TreeNode[] Nodes { get; protected set; }
+ public SelectionModel Selection { get; }
+
public TreeNode SelectedNode
{
- get => _selected;
- set
+ get => _selectedNode;
+ private set
{
if (Details != null)
{
_propertyFilter = Details.PropertyFilter;
}
- if (RaiseAndSetIfChanged(ref _selected, value))
+ if (RaiseAndSetIfChanged(ref _selectedNode, value))
{
Details = value != null ?
new ControlDetailsViewModel(value.Visual, _propertyFilter) :
@@ -83,8 +95,8 @@ namespace Avalonia.Diagnostics.ViewModels
if (node != null)
{
- SelectedNode = node;
ExpandNode(node.Parent);
+ Selection.SelectedIndex = node.Index;
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
index a1e6ca7d37..4ddb320175 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
@@ -6,7 +6,7 @@
+ Selection="{Binding Selection}">
diff --git a/src/Avalonia.Layout/ILayoutRoot.cs b/src/Avalonia.Layout/ILayoutRoot.cs
index 56aca75871..e2f16b338a 100644
--- a/src/Avalonia.Layout/ILayoutRoot.cs
+++ b/src/Avalonia.Layout/ILayoutRoot.cs
@@ -10,11 +10,6 @@ namespace Avalonia.Layout
///
Size ClientSize { get; }
- ///
- /// The maximum client size available.
- ///
- Size MaxClientSize { get; }
-
///
/// The scaling factor to use in layout.
///
diff --git a/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs
index e81c8853e8..8aa9b1a122 100644
--- a/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs
+++ b/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs
@@ -9,8 +9,7 @@ namespace Avalonia.Native
{
}
- public override Point TranslatePoint(Point pt) => pt;
- public override Size TranslateSize(Size size) => size;
+ public override double Scaling => 1;
}
}
diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs
index a7ca528b2b..9783454e0e 100644
--- a/src/Avalonia.Native/WindowImplBase.cs
+++ b/src/Avalonia.Native/WindowImplBase.cs
@@ -336,7 +336,7 @@ namespace Avalonia.Native
_native.BeginMoveDrag();
}
- public Size MaxClientSize => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
+ public Size MaxAutoSizeHint => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
.OrderByDescending(x => x.Width + x.Height).FirstOrDefault();
public void SetTopmost(bool value)
diff --git a/src/Avalonia.Themes.Default/DatePicker.xaml b/src/Avalonia.Themes.Default/CalendarDatePicker.xaml
similarity index 97%
rename from src/Avalonia.Themes.Default/DatePicker.xaml
rename to src/Avalonia.Themes.Default/CalendarDatePicker.xaml
index 7adb1c2d5f..bc1aba1a03 100644
--- a/src/Avalonia.Themes.Default/DatePicker.xaml
+++ b/src/Avalonia.Themes.Default/CalendarDatePicker.xaml
@@ -8,7 +8,7 @@
-
-
diff --git a/src/Avalonia.Themes.Default/ContextMenu.xaml b/src/Avalonia.Themes.Default/ContextMenu.xaml
index 75f8f7c23d..9b84253c8a 100644
--- a/src/Avalonia.Themes.Default/ContextMenu.xaml
+++ b/src/Avalonia.Themes.Default/ContextMenu.xaml
@@ -10,7 +10,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
-
+
-
+
+
diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml
index 314416cda0..d7f367c591 100644
--- a/src/Avalonia.Themes.Default/MenuItem.xaml
+++ b/src/Avalonia.Themes.Default/MenuItem.xaml
@@ -113,7 +113,7 @@
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Default/Slider.xaml b/src/Avalonia.Themes.Default/Slider.xaml
index b21cbf3650..1d48a946fc 100644
--- a/src/Avalonia.Themes.Default/Slider.xaml
+++ b/src/Avalonia.Themes.Default/Slider.xaml
@@ -46,7 +46,7 @@
-
+
diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml
new file mode 100644
index 0000000000..ed172b52ab
--- /dev/null
+++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml
@@ -0,0 +1,294 @@
+
+
+ 0,0,0,6
+ 6
+ 6
+ 154
+ 20
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml
index 44318ffa8f..0bea6c5781 100644
--- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml
@@ -138,6 +138,9 @@
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml
index e43a7ab4e7..ef296faa60 100644
--- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml
@@ -138,6 +138,9 @@
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml
index 7a715bbde7..ae2218ddde 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml
@@ -1,4 +1,6 @@
-
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml
index 5c6286a0bc..9510875212 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml
@@ -1,4 +1,6 @@
-
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
index 7364c339f1..eb6cc610cc 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
@@ -1,6 +1,6 @@
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
index 15e157f573..cba20faf7c 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
@@ -1,6 +1,6 @@
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml
index 5b6da70233..d29db79e7b 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml
@@ -5,4 +5,5 @@
+
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml
index 5825a2bf34..43867f6e97 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml
@@ -5,4 +5,5 @@
+
diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml
index 42fcee6cd5..345a74512c 100644
--- a/src/Avalonia.Themes.Fluent/Button.xaml
+++ b/src/Avalonia.Themes.Fluent/Button.xaml
@@ -24,21 +24,19 @@
+ -->
-
-
+
@@ -46,43 +44,43 @@
-
-
-
diff --git a/src/Avalonia.Themes.Fluent/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/ButtonSpinner.xaml
index 89fbb9d64d..e196fcd7a7 100644
--- a/src/Avalonia.Themes.Fluent/ButtonSpinner.xaml
+++ b/src/Avalonia.Themes.Fluent/ButtonSpinner.xaml
@@ -1,104 +1,140 @@
-
-
-
-
-
-
-
-
+
-
-
-
+
diff --git a/src/Avalonia.Themes.Fluent/Calendar.xaml b/src/Avalonia.Themes.Fluent/Calendar.xaml
index 6bbee4ef17..6ae334dbae 100644
--- a/src/Avalonia.Themes.Fluent/Calendar.xaml
+++ b/src/Avalonia.Themes.Fluent/Calendar.xaml
@@ -5,13 +5,14 @@
// All other rights reserved.
-->
-
+
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Themes.Fluent/CalendarButton.xaml b/src/Avalonia.Themes.Fluent/CalendarButton.xaml
index 3314534b3b..87082ef21e 100644
--- a/src/Avalonia.Themes.Fluent/CalendarButton.xaml
+++ b/src/Avalonia.Themes.Fluent/CalendarButton.xaml
@@ -7,75 +7,115 @@
-
- -->
+
+
-
-
-
-
+
+
+
+
+
+
-
-
-
- -->
+
+
diff --git a/src/Avalonia.Themes.Fluent/DatePicker.xaml b/src/Avalonia.Themes.Fluent/CalendarDatePicker.xaml
similarity index 97%
rename from src/Avalonia.Themes.Fluent/DatePicker.xaml
rename to src/Avalonia.Themes.Fluent/CalendarDatePicker.xaml
index 7adb1c2d5f..bc1aba1a03 100644
--- a/src/Avalonia.Themes.Fluent/DatePicker.xaml
+++ b/src/Avalonia.Themes.Fluent/CalendarDatePicker.xaml
@@ -8,7 +8,7 @@
-
-
diff --git a/src/Avalonia.Themes.Fluent/CalendarDayButton.xaml b/src/Avalonia.Themes.Fluent/CalendarDayButton.xaml
index 2d79e62a75..ff72044c93 100644
--- a/src/Avalonia.Themes.Fluent/CalendarDayButton.xaml
+++ b/src/Avalonia.Themes.Fluent/CalendarDayButton.xaml
@@ -7,110 +7,110 @@
-
-
-
-
+ -->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/src/Avalonia.Themes.Fluent/CalendarItem.xaml b/src/Avalonia.Themes.Fluent/CalendarItem.xaml
index dfd89ed82f..df17da84dc 100644
--- a/src/Avalonia.Themes.Fluent/CalendarItem.xaml
+++ b/src/Avalonia.Themes.Fluent/CalendarItem.xaml
@@ -8,176 +8,127 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ FontSize="12"/>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
diff --git a/src/Avalonia.Themes.Fluent/Common.xaml b/src/Avalonia.Themes.Fluent/Common.xaml
new file mode 100644
index 0000000000..e09e39d7cb
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/Common.xaml
@@ -0,0 +1,6 @@
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/ContextMenu.xaml b/src/Avalonia.Themes.Fluent/ContextMenu.xaml
index 75f8f7c23d..44783a8dea 100644
--- a/src/Avalonia.Themes.Fluent/ContextMenu.xaml
+++ b/src/Avalonia.Themes.Fluent/ContextMenu.xaml
@@ -1,22 +1,61 @@
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Themes.Fluent/MenuItem.xaml b/src/Avalonia.Themes.Fluent/MenuItem.xaml
index 314416cda0..fbb994e90c 100644
--- a/src/Avalonia.Themes.Fluent/MenuItem.xaml
+++ b/src/Avalonia.Themes.Fluent/MenuItem.xaml
@@ -2,98 +2,143 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
xmlns:sys="clr-namespace:System;assembly=netstandard">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+ 0,4,0,4
+ 0,0,12,0
+ 24,0,0,0
+ M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z
+
-
-
-
+
+
+
+
+
+
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/NumericUpDown.xaml
index 24cbb62908..08de50c6e3 100644
--- a/src/Avalonia.Themes.Fluent/NumericUpDown.xaml
+++ b/src/Avalonia.Themes.Fluent/NumericUpDown.xaml
@@ -1,38 +1,60 @@
-
+
+
+
+
+
+
+
+
+
+
-
-
\ No newline at end of file
+
+
diff --git a/src/Avalonia.Themes.Fluent/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/RepeatButton.xaml
index 702e4e6ebd..12ba38d614 100644
--- a/src/Avalonia.Themes.Fluent/RepeatButton.xaml
+++ b/src/Avalonia.Themes.Fluent/RepeatButton.xaml
@@ -1,40 +1,62 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+ 8,5,8,6
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Separator.xaml b/src/Avalonia.Themes.Fluent/Separator.xaml
index cf0db16ee6..dc968fe86c 100644
--- a/src/Avalonia.Themes.Fluent/Separator.xaml
+++ b/src/Avalonia.Themes.Fluent/Separator.xaml
@@ -4,17 +4,15 @@
-
-
-
diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml
index b21cbf3650..539c448e0f 100644
--- a/src/Avalonia.Themes.Fluent/Slider.xaml
+++ b/src/Avalonia.Themes.Fluent/Slider.xaml
@@ -1,93 +1,264 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0,0,0,4
+ 15
+ 15
+ 32
+ 32
+ 10
+ 20
+ 20
+ 20
+ 20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/src/Avalonia.Themes.Fluent/TextBox.xaml b/src/Avalonia.Themes.Fluent/TextBox.xaml
index e89cf2b49c..49fc4b59b0 100644
--- a/src/Avalonia.Themes.Fluent/TextBox.xaml
+++ b/src/Avalonia.Themes.Fluent/TextBox.xaml
@@ -27,6 +27,7 @@
Grid.ColumnSpan="2"
TextBlock.FontWeight="Normal"
TextBlock.Foreground="{DynamicResource TextControlHeaderForeground}"
+ IsVisible="False"
Margin="{DynamicResource TextBoxTopHeaderMargin}" />
+
+
+
+
+
+
+
+
+
+
+
+
+ 8,5,8,6
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml
new file mode 100644
index 0000000000..ed172b52ab
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml
@@ -0,0 +1,294 @@
+
+
+ 0,0,0,6
+ 6
+ 6
+ 154
+ 20
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/ToolTip.xaml b/src/Avalonia.Themes.Fluent/ToolTip.xaml
index 1fc0202dd3..cf6f32f9bc 100644
--- a/src/Avalonia.Themes.Fluent/ToolTip.xaml
+++ b/src/Avalonia.Themes.Fluent/ToolTip.xaml
@@ -1,17 +1,78 @@
-
\ No newline at end of file
+
+
+
+
+ Hover Here
+
+
+
+
+
+ ToolTip
+ A control which pops up a hint when a control is hovered
+
+
+ ToolTip bottom placement
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs
index 1f1590bdcd..bb1c0da902 100644
--- a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs
+++ b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs
@@ -1,6 +1,8 @@
using System;
+using System.Reactive.Disposables;
using Avalonia.Logging;
using Avalonia.Media;
+using Avalonia.Media.Transformation;
namespace Avalonia.Animation.Animators
{
@@ -19,6 +21,12 @@ namespace Avalonia.Animation.Animators
// Check if the Target Property is Transform derived.
if (typeof(Transform).IsAssignableFrom(Property.OwnerType))
{
+ if (ctrl.RenderTransform is TransformOperations)
+ {
+ // HACK: This animator cannot reasonably animate CSS transforms at the moment.
+ return Disposable.Empty;
+ }
+
if (ctrl.RenderTransform == null)
{
var normalTransform = new TransformGroup();
@@ -51,7 +59,7 @@ namespace Avalonia.Animation.Animators
// It's a transform object so let's target that.
if (renderTransformType == Property.OwnerType)
{
- return _doubleAnimator.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete);
+ return _doubleAnimator.Apply(animation, (Transform) ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete);
}
// It's a TransformGroup and try finding the target there.
else if (renderTransformType == typeof(TransformGroup))
diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs
new file mode 100644
index 0000000000..8e9d20eb8f
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs
@@ -0,0 +1,35 @@
+using System;
+using Avalonia.Media;
+using Avalonia.Media.Transformation;
+
+namespace Avalonia.Animation.Animators
+{
+ public class TransformOperationsAnimator : Animator
+ {
+ public TransformOperationsAnimator()
+ {
+ Validate = ValidateTransform;
+ }
+
+ public override TransformOperations Interpolate(double progress, TransformOperations oldValue, TransformOperations newValue)
+ {
+ var oldTransform = EnsureOperations(oldValue);
+ var newTransform = EnsureOperations(newValue);
+
+ return TransformOperations.Interpolate(oldTransform, newTransform, progress);
+ }
+
+ internal static TransformOperations EnsureOperations(ITransform value)
+ {
+ return value as TransformOperations ?? TransformOperations.Identity;
+ }
+
+ private void ValidateTransform(AnimatorKeyFrame kf)
+ {
+ if (!(kf.Value is TransformOperations))
+ {
+ throw new InvalidOperationException($"All keyframes must be of type {typeof(TransformOperations)}.");
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs
new file mode 100644
index 0000000000..104acb71ad
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
+using Avalonia.Media;
+
+namespace Avalonia.Animation
+{
+ public class TransformOperationsTransition : Transition
+ {
+ private static readonly TransformOperationsAnimator _operationsAnimator = new TransformOperationsAnimator();
+
+ 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);
+ });
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs
index 898c6027a5..206b842220 100644
--- a/src/Avalonia.Visuals/Matrix.cs
+++ b/src/Avalonia.Visuals/Matrix.cs
@@ -54,7 +54,7 @@ namespace Avalonia
///
/// HasInverse Property - returns true if this matrix is invertible, false otherwise.
///
- public bool HasInverse => GetDeterminant() != 0;
+ public bool HasInverse => !MathUtilities.IsZero(GetDeterminant());
///
/// The first element of the first row
@@ -286,7 +286,7 @@ namespace Avalonia
{
double d = GetDeterminant();
- if (d == 0)
+ if (MathUtilities.IsZero(d))
{
throw new InvalidOperationException("Transform is not invertible.");
}
@@ -319,5 +319,76 @@ namespace Avalonia
);
}
}
+
+ ///
+ /// Decomposes given matrix into transform operations.
+ ///
+ /// Matrix to decompose.
+ /// Decomposed matrix.
+ /// The status of the operation.
+ public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed)
+ {
+ decomposed = default;
+
+ var determinant = matrix.GetDeterminant();
+
+ if (MathUtilities.IsZero(determinant))
+ {
+ return false;
+ }
+
+ var m11 = matrix.M11;
+ var m21 = matrix.M21;
+ var m12 = matrix.M12;
+ var m22 = matrix.M22;
+
+ // Translation.
+ decomposed.Translate = new Vector(matrix.M31, matrix.M32);
+
+ // Scale sign.
+ var scaleX = 1d;
+ var scaleY = 1d;
+
+ if (determinant < 0)
+ {
+ if (m11 < m22)
+ {
+ scaleX *= -1d;
+ }
+ else
+ {
+ scaleY *= -1d;
+ }
+ }
+
+ // X Scale.
+ scaleX *= Math.Sqrt(m11 * m11 + m12 * m12);
+
+ m11 /= scaleX;
+ m12 /= scaleX;
+
+ // XY Shear.
+ double scaledShear = m11 * m21 + m12 * m22;
+
+ m21 -= m11 * scaledShear;
+ m22 -= m12 * scaledShear;
+
+ // Y Scale.
+ scaleY *= Math.Sqrt(m21 * m21 + m22 * m22);
+
+ decomposed.Scale = new Vector(scaleX, scaleY);
+ decomposed.Skew = new Vector(scaledShear / scaleY, 0d);
+ decomposed.Angle = Math.Atan2(m12, m11);
+
+ return true;
+ }
+
+ public struct Decomposed
+ {
+ public Vector Translate;
+ public Vector Scale;
+ public Vector Skew;
+ public double Angle;
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs
index 2e06d2578f..052ee5e1b7 100644
--- a/src/Avalonia.Visuals/Media/Color.cs
+++ b/src/Avalonia.Visuals/Media/Color.cs
@@ -89,33 +89,64 @@ namespace Avalonia.Media
/// The .
public static Color Parse(string s)
{
- if (s == null) throw new ArgumentNullException(nameof(s));
- if (s.Length == 0) throw new FormatException();
+ if (TryParse(s, out Color color))
+ {
+ return color;
+ }
- if (s[0] == '#')
+ throw new FormatException($"Invalid color string: '{s}'.");
+ }
+
+ ///
+ /// Parses a color string.
+ ///
+ /// The color string.
+ /// The .
+ public static Color Parse(ReadOnlySpan s)
+ {
+ if (TryParse(s, out Color color))
{
- var or = 0u;
+ return color;
+ }
- if (s.Length == 7)
- {
- or = 0xff000000;
- }
- else if (s.Length != 9)
- {
- throw new FormatException($"Invalid color string: '{s}'.");
- }
+ throw new FormatException($"Invalid color string: '{s.ToString()}'.");
+ }
- return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture) | or);
+ ///
+ /// Parses a color string.
+ ///
+ /// The color string.
+ /// The parsed color
+ /// The status of the operation.
+ public static bool TryParse(string s, out Color color)
+ {
+ if (s == null)
+ {
+ throw new ArgumentNullException(nameof(s));
+ }
+
+ if (s.Length == 0)
+ {
+ throw new FormatException();
+ }
+
+ if (s[0] == '#' && TryParseInternal(s.AsSpan(), out color))
+ {
+ return true;
}
var knownColor = KnownColors.GetKnownColor(s);
if (knownColor != KnownColor.None)
{
- return knownColor.ToColor();
+ color = knownColor.ToColor();
+
+ return true;
}
- throw new FormatException($"Invalid color string: '{s}'.");
+ color = default;
+
+ return false;
}
///
@@ -126,40 +157,79 @@ namespace Avalonia.Media
/// The status of the operation.
public static bool TryParse(ReadOnlySpan s, out Color color)
{
- color = default;
- if (s == null)
- return false;
if (s.Length == 0)
+ {
+ color = default;
+
return false;
+ }
if (s[0] == '#')
{
- var or = 0u;
+ return TryParseInternal(s, out color);
+ }
+
+ var knownColor = KnownColors.GetKnownColor(s.ToString());
+
+ if (knownColor != KnownColor.None)
+ {
+ color = knownColor.ToColor();
+
+ return true;
+ }
+
+ color = default;
+
+ return false;
+ }
+
+ private static bool TryParseInternal(ReadOnlySpan s, out Color color)
+ {
+ static bool TryParseCore(ReadOnlySpan input, ref Color color)
+ {
+ var alphaComponent = 0u;
- if (s.Length == 7)
+ if (input.Length == 6)
{
- or = 0xff000000;
+ alphaComponent = 0xff000000;
}
- else if (s.Length != 9)
+ else if (input.Length != 8)
{
return false;
}
- if(!uint.TryParse(s.Slice(1).ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
+ // TODO: (netstandard 2.1) Can use allocation free parsing.
+ if (!uint.TryParse(input.ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture,
+ out var parsed))
+ {
return false;
- color = FromUInt32(parsed| or);
+ }
+
+ color = FromUInt32(parsed | alphaComponent);
+
return true;
}
- var knownColor = KnownColors.GetKnownColor(s.ToString());
+ color = default;
- if (knownColor != KnownColor.None)
+ ReadOnlySpan input = s.Slice(1);
+
+ // Handle shorthand cases like #FFF (RGB) or #FFFF (ARGB).
+ if (input.Length == 3 || input.Length == 4)
{
- color = knownColor.ToColor();
- return true;
+ var extendedLength = 2 * input.Length;
+ Span extended = stackalloc char[extendedLength];
+
+ for (int i = 0; i < input.Length; i++)
+ {
+ extended[2 * i + 0] = input[i];
+ extended[2 * i + 1] = input[i];
+ }
+
+ return TryParseCore(extended, ref color);
}
- return false;
+ return TryParseCore(input, ref color);
}
///
diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs
index 190675fe48..ba7191d7a6 100644
--- a/src/Avalonia.Visuals/Media/DrawingContext.cs
+++ b/src/Avalonia.Visuals/Media/DrawingContext.cs
@@ -4,6 +4,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
+using Avalonia.Utilities;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Media
@@ -154,12 +155,12 @@ namespace Avalonia.Media
return;
}
- if (Math.Abs(radiusX) > double.Epsilon)
+ if (!MathUtilities.IsZero(radiusX))
{
radiusX = Math.Min(radiusX, rect.Width / 2);
}
- if (Math.Abs(radiusY) > double.Epsilon)
+ if (!MathUtilities.IsZero(radiusY))
{
radiusY = Math.Min(radiusY, rect.Height / 2);
}
diff --git a/src/Avalonia.Visuals/Media/IMutableTransform.cs b/src/Avalonia.Visuals/Media/IMutableTransform.cs
new file mode 100644
index 0000000000..2033c434c0
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/IMutableTransform.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Avalonia.Media
+{
+ public interface IMutableTransform : ITransform
+ {
+ ///
+ /// Raised when the transform changes.
+ ///
+ event EventHandler Changed;
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/ITransform.cs b/src/Avalonia.Visuals/Media/ITransform.cs
new file mode 100644
index 0000000000..91577fe38e
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/ITransform.cs
@@ -0,0 +1,10 @@
+using System.ComponentModel;
+
+namespace Avalonia.Media
+{
+ [TypeConverter(typeof(TransformConverter))]
+ public interface ITransform
+ {
+ Matrix Value { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
index 720185a3ad..0292398782 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
@@ -4,6 +4,7 @@ using System.Linq;
using Avalonia.Media.Immutable;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
+using Avalonia.Utilities;
using Avalonia.Utility;
namespace Avalonia.Media.TextFormatting
@@ -184,7 +185,7 @@ namespace Avalonia.Media.TextFormatting
///
private void UpdateLayout()
{
- if (_text.IsEmpty || Math.Abs(MaxWidth) < double.Epsilon || Math.Abs(MaxHeight) < double.Epsilon)
+ if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{
var textLine = CreateEmptyTextLine(0);
diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs
index 70ef1eaaf4..7cf1b35ada 100644
--- a/src/Avalonia.Visuals/Media/Transform.cs
+++ b/src/Avalonia.Visuals/Media/Transform.cs
@@ -8,11 +8,12 @@ namespace Avalonia.Media
///
/// Represents a transform on an .
///
- public abstract class Transform : Animatable
+ public abstract class Transform : Animatable, IMutableTransform
{
static Transform()
{
- Animation.Animation.RegisterAnimator(prop => typeof(Transform).IsAssignableFrom(prop.OwnerType));
+ Animation.Animation.RegisterAnimator(prop =>
+ typeof(ITransform).IsAssignableFrom(prop.OwnerType));
}
///
diff --git a/src/Avalonia.Visuals/Media/TransformConverter.cs b/src/Avalonia.Visuals/Media/TransformConverter.cs
new file mode 100644
index 0000000000..e79c0b8b7b
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TransformConverter.cs
@@ -0,0 +1,23 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using Avalonia.Media.Transformation;
+
+namespace Avalonia.Media
+{
+ ///
+ /// Creates an from a string representation.
+ ///
+ public class TransformConverter : TypeConverter
+ {
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ return sourceType == typeof(string);
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ return TransformOperations.Parse((string)value);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs
new file mode 100644
index 0000000000..1e80eabfc8
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs
@@ -0,0 +1,40 @@
+namespace Avalonia.Media.Transformation
+{
+ internal static class InterpolationUtilities
+ {
+ public static double InterpolateScalars(double from, double to, double progress)
+ {
+ return from * (1d - progress) + to * progress;
+ }
+
+ public static Vector InterpolateVectors(Vector from, Vector to, double progress)
+ {
+ var x = InterpolateScalars(from.X, to.X, progress);
+ var y = InterpolateScalars(from.Y, to.Y, progress);
+
+ return new Vector(x, y);
+ }
+
+ public static Matrix ComposeTransform(Matrix.Decomposed decomposed)
+ {
+ // According to https://www.w3.org/TR/css-transforms-1/#recomposing-to-a-2d-matrix
+
+ return Matrix.CreateTranslation(decomposed.Translate) *
+ Matrix.CreateRotation(decomposed.Angle) *
+ Matrix.CreateSkew(decomposed.Skew.X, decomposed.Skew.Y) *
+ Matrix.CreateScale(decomposed.Scale);
+ }
+
+ public static Matrix.Decomposed InterpolateDecomposedTransforms(ref Matrix.Decomposed from, ref Matrix.Decomposed to, double progres)
+ {
+ Matrix.Decomposed result = default;
+
+ result.Translate = InterpolateVectors(from.Translate, to.Translate, progres);
+ result.Scale = InterpolateVectors(from.Scale, to.Scale, progres);
+ result.Skew = InterpolateVectors(from.Skew, to.Skew, progres);
+ result.Angle = InterpolateScalars(from.Angle, to.Angle, progres);
+
+ return result;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs b/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs
new file mode 100644
index 0000000000..36f5dd98f1
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs
@@ -0,0 +1,230 @@
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Media.Transformation
+{
+ ///
+ /// Represents a single primitive transform (like translation, rotation, scale, etc.).
+ ///
+ public struct TransformOperation
+ {
+ public OperationType Type;
+ public Matrix Matrix;
+ public DataLayout Data;
+
+ public enum OperationType
+ {
+ Translate,
+ Rotate,
+ Scale,
+ Skew,
+ Matrix,
+ Identity
+ }
+
+ ///
+ /// Returns whether operation produces the identity matrix.
+ ///
+ public bool IsIdentity => Matrix.IsIdentity;
+
+ ///
+ /// Bakes this operation to a transform matrix.
+ ///
+ public void Bake()
+ {
+ Matrix = Matrix.Identity;
+
+ switch (Type)
+ {
+ case OperationType.Translate:
+ {
+ Matrix = Matrix.CreateTranslation(Data.Translate.X, Data.Translate.Y);
+
+ break;
+ }
+ case OperationType.Rotate:
+ {
+ Matrix = Matrix.CreateRotation(Data.Rotate.Angle);
+
+ break;
+ }
+ case OperationType.Scale:
+ {
+ Matrix = Matrix.CreateScale(Data.Scale.X, Data.Scale.Y);
+
+ break;
+ }
+ case OperationType.Skew:
+ {
+ Matrix = Matrix.CreateSkew(Data.Skew.X, Data.Skew.Y);
+
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Returns new identity transform operation.
+ ///
+ public static TransformOperation Identity =>
+ new TransformOperation { Matrix = Matrix.Identity, Type = OperationType.Identity };
+
+ ///
+ /// Attempts to interpolate between two transform operations.
+ ///
+ /// Source operation.
+ /// Target operation.
+ /// Interpolation progress.
+ /// Interpolation result that will be filled in when operation was successful.
+ ///
+ /// Based upon https://www.w3.org/TR/css-transforms-1/#interpolation-of-transform-functions.
+ ///
+ public static bool TryInterpolate(TransformOperation? from, TransformOperation? to, double progress,
+ ref TransformOperation result)
+ {
+ bool fromIdentity = IsOperationIdentity(ref from);
+ bool toIdentity = IsOperationIdentity(ref to);
+
+ if (fromIdentity && toIdentity)
+ {
+ return true;
+ }
+
+ // ReSharper disable PossibleInvalidOperationException
+ TransformOperation fromValue = fromIdentity ? Identity : from.Value;
+ TransformOperation toValue = toIdentity ? Identity : to.Value;
+ // ReSharper restore PossibleInvalidOperationException
+
+ var interpolationType = toIdentity ? fromValue.Type : toValue.Type;
+
+ result.Type = interpolationType;
+
+ switch (interpolationType)
+ {
+ case OperationType.Translate:
+ {
+ double fromX = fromIdentity ? 0 : fromValue.Data.Translate.X;
+ double fromY = fromIdentity ? 0 : fromValue.Data.Translate.Y;
+
+ double toX = toIdentity ? 0 : toValue.Data.Translate.X;
+ double toY = toIdentity ? 0 : toValue.Data.Translate.Y;
+
+ result.Data.Translate.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress);
+ result.Data.Translate.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress);
+
+ result.Bake();
+
+ break;
+ }
+ case OperationType.Rotate:
+ {
+ double fromAngle = fromIdentity ? 0 : fromValue.Data.Rotate.Angle;
+
+ double toAngle = toIdentity ? 0 : toValue.Data.Rotate.Angle;
+
+ result.Data.Rotate.Angle = InterpolationUtilities.InterpolateScalars(fromAngle, toAngle, progress);
+
+ result.Bake();
+
+ break;
+ }
+ case OperationType.Scale:
+ {
+ double fromX = fromIdentity ? 1 : fromValue.Data.Scale.X;
+ double fromY = fromIdentity ? 1 : fromValue.Data.Scale.Y;
+
+ double toX = toIdentity ? 1 : toValue.Data.Scale.X;
+ double toY = toIdentity ? 1 : toValue.Data.Scale.Y;
+
+ result.Data.Scale.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress);
+ result.Data.Scale.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress);
+
+ result.Bake();
+
+ break;
+ }
+ case OperationType.Skew:
+ {
+ double fromX = fromIdentity ? 0 : fromValue.Data.Skew.X;
+ double fromY = fromIdentity ? 0 : fromValue.Data.Skew.Y;
+
+ double toX = toIdentity ? 0 : toValue.Data.Skew.X;
+ double toY = toIdentity ? 0 : toValue.Data.Skew.Y;
+
+ result.Data.Skew.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress);
+ result.Data.Skew.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress);
+
+ result.Bake();
+
+ break;
+ }
+ case OperationType.Matrix:
+ {
+ var fromMatrix = fromIdentity ? Matrix.Identity : fromValue.Matrix;
+ var toMatrix = toIdentity ? Matrix.Identity : toValue.Matrix;
+
+ if (!Matrix.TryDecomposeTransform(fromMatrix, out Matrix.Decomposed fromDecomposed) ||
+ !Matrix.TryDecomposeTransform(toMatrix, out Matrix.Decomposed toDecomposed))
+ {
+ return false;
+ }
+
+ var interpolated =
+ InterpolationUtilities.InterpolateDecomposedTransforms(
+ ref fromDecomposed, ref toDecomposed,
+ progress);
+
+ result.Matrix = InterpolationUtilities.ComposeTransform(interpolated);
+
+ break;
+ }
+ case OperationType.Identity:
+ {
+ // Do nothing.
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool IsOperationIdentity(ref TransformOperation? operation)
+ {
+ return !operation.HasValue || operation.Value.IsIdentity;
+ }
+
+ [StructLayout(LayoutKind.Explicit)]
+ public struct DataLayout
+ {
+ [FieldOffset(0)] public SkewLayout Skew;
+
+ [FieldOffset(0)] public ScaleLayout Scale;
+
+ [FieldOffset(0)] public TranslateLayout Translate;
+
+ [FieldOffset(0)] public RotateLayout Rotate;
+
+ public struct SkewLayout
+ {
+ public double X;
+ public double Y;
+ }
+
+ public struct ScaleLayout
+ {
+ public double X;
+ public double Y;
+ }
+
+ public struct TranslateLayout
+ {
+ public double X;
+ public double Y;
+ }
+
+ public struct RotateLayout
+ {
+ public double Angle;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs b/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs
new file mode 100644
index 0000000000..334bb93562
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs
@@ -0,0 +1,258 @@
+using System;
+using System.Collections.Generic;
+
+namespace Avalonia.Media.Transformation
+{
+ ///
+ /// Contains a list of that represent primitive transforms that will be
+ /// applied in declared order.
+ ///
+ public sealed class TransformOperations : ITransform
+ {
+ public static TransformOperations Identity { get; } = new TransformOperations(new List());
+
+ private readonly List _operations;
+
+ private TransformOperations(List operations)
+ {
+ _operations = operations ?? throw new ArgumentNullException(nameof(operations));
+
+ IsIdentity = CheckIsIdentity();
+
+ Value = ApplyTransforms();
+ }
+
+ ///
+ /// Returns whether all operations combined together produce the identity matrix.
+ ///
+ public bool IsIdentity { get; }
+
+ public IReadOnlyList Operations => _operations;
+
+ public Matrix Value { get; }
+
+ public static TransformOperations Parse(string s)
+ {
+ return TransformParser.Parse(s);
+ }
+
+ public static Builder CreateBuilder(int capacity)
+ {
+ return new Builder(capacity);
+ }
+
+ public static TransformOperations Interpolate(TransformOperations from, TransformOperations to, double progress)
+ {
+ TransformOperations result = Identity;
+
+ if (!TryInterpolate(from, to, progress, ref result))
+ {
+ // If the matrices cannot be interpolated, fallback to discrete animation logic.
+ // See https://drafts.csswg.org/css-transforms/#matrix-interpolation
+ result = progress < 0.5 ? from : to;
+ }
+
+ return result;
+ }
+
+ private Matrix ApplyTransforms(int startOffset = 0)
+ {
+ Matrix matrix = Matrix.Identity;
+
+ for (var i = startOffset; i < _operations.Count; i++)
+ {
+ TransformOperation operation = _operations[i];
+ matrix *= operation.Matrix;
+ }
+
+ return matrix;
+ }
+
+ private bool CheckIsIdentity()
+ {
+ foreach (TransformOperation operation in _operations)
+ {
+ if (!operation.IsIdentity)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool TryInterpolate(TransformOperations from, TransformOperations to, double progress, ref TransformOperations result)
+ {
+ bool fromIdentity = from.IsIdentity;
+ bool toIdentity = to.IsIdentity;
+
+ if (fromIdentity && toIdentity)
+ {
+ return true;
+ }
+
+ int matchingPrefixLength = ComputeMatchingPrefixLength(from, to);
+ int fromSize = fromIdentity ? 0 : from._operations.Count;
+ int toSize = toIdentity ? 0 : to._operations.Count;
+ int numOperations = Math.Max(fromSize, toSize);
+
+ var builder = new Builder(matchingPrefixLength);
+
+ for (int i = 0; i < matchingPrefixLength; i++)
+ {
+ TransformOperation interpolated = new TransformOperation
+ {
+ Type = TransformOperation.OperationType.Identity
+ };
+
+ if (!TransformOperation.TryInterpolate(
+ i >= fromSize ? default(TransformOperation?) : from._operations[i],
+ i >= toSize ? default(TransformOperation?) : to._operations[i],
+ progress,
+ ref interpolated))
+ {
+ return false;
+ }
+
+ builder.Append(interpolated);
+ }
+
+ if (matchingPrefixLength < numOperations)
+ {
+ if (!ComputeDecomposedTransform(from, matchingPrefixLength, out Matrix.Decomposed fromDecomposed) ||
+ !ComputeDecomposedTransform(to, matchingPrefixLength, out Matrix.Decomposed toDecomposed))
+ {
+ return false;
+ }
+
+ var transform = InterpolationUtilities.InterpolateDecomposedTransforms(ref fromDecomposed, ref toDecomposed, progress);
+
+ builder.AppendMatrix(InterpolationUtilities.ComposeTransform(transform));
+ }
+
+ result = builder.Build();
+
+ return true;
+ }
+
+ private static bool ComputeDecomposedTransform(TransformOperations operations, int startOffset, out Matrix.Decomposed decomposed)
+ {
+ Matrix transform = operations.ApplyTransforms(startOffset);
+
+ if (!Matrix.TryDecomposeTransform(transform, out decomposed))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static int ComputeMatchingPrefixLength(TransformOperations from, TransformOperations to)
+ {
+ int numOperations = Math.Min(from._operations.Count, to._operations.Count);
+
+ for (int i = 0; i < numOperations; i++)
+ {
+ if (from._operations[i].Type != to._operations[i].Type)
+ {
+ return i;
+ }
+ }
+
+ // If the operations match to the length of the shorter list, then pad its
+ // length with the matching identity operations.
+ // https://drafts.csswg.org/css-transforms/#transform-function-lists
+ return Math.Max(from._operations.Count, to._operations.Count);
+ }
+
+ public readonly struct Builder
+ {
+ private readonly List _operations;
+
+ public Builder(int capacity)
+ {
+ _operations = new List(capacity);
+ }
+
+ public void AppendTranslate(double x, double y)
+ {
+ var toAdd = new TransformOperation();
+
+ toAdd.Type = TransformOperation.OperationType.Translate;
+ toAdd.Data.Translate.X = x;
+ toAdd.Data.Translate.Y = y;
+
+ toAdd.Bake();
+
+ _operations.Add(toAdd);
+ }
+
+ public void AppendRotate(double angle)
+ {
+ var toAdd = new TransformOperation();
+
+ toAdd.Type = TransformOperation.OperationType.Rotate;
+ toAdd.Data.Rotate.Angle = angle;
+
+ toAdd.Bake();
+
+ _operations.Add(toAdd);
+ }
+
+ public void AppendScale(double x, double y)
+ {
+ var toAdd = new TransformOperation();
+
+ toAdd.Type = TransformOperation.OperationType.Scale;
+ toAdd.Data.Scale.X = x;
+ toAdd.Data.Scale.Y = y;
+
+ toAdd.Bake();
+
+ _operations.Add(toAdd);
+ }
+
+ public void AppendSkew(double x, double y)
+ {
+ var toAdd = new TransformOperation();
+
+ toAdd.Type = TransformOperation.OperationType.Skew;
+ toAdd.Data.Skew.X = x;
+ toAdd.Data.Skew.Y = y;
+
+ toAdd.Bake();
+
+ _operations.Add(toAdd);
+ }
+
+ public void AppendMatrix(Matrix matrix)
+ {
+ var toAdd = new TransformOperation();
+
+ toAdd.Type = TransformOperation.OperationType.Matrix;
+ toAdd.Matrix = matrix;
+
+ _operations.Add(toAdd);
+ }
+
+ public void AppendIdentity()
+ {
+ var toAdd = new TransformOperation();
+
+ toAdd.Type = TransformOperation.OperationType.Identity;
+
+ _operations.Add(toAdd);
+ }
+
+ public void Append(TransformOperation toAdd)
+ {
+ _operations.Add(toAdd);
+ }
+
+ public TransformOperations Build()
+ {
+ return new TransformOperations(_operations);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs b/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs
new file mode 100644
index 0000000000..85f4f5fec1
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs
@@ -0,0 +1,444 @@
+using System;
+using System.Globalization;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.Transformation
+{
+ public static class TransformParser
+ {
+ private static readonly (string, TransformFunction)[] s_functionMapping =
+ {
+ ("translate", TransformFunction.Translate),
+ ("translateX", TransformFunction.TranslateX),
+ ("translateY", TransformFunction.TranslateY),
+ ("scale", TransformFunction.Scale),
+ ("scaleX", TransformFunction.ScaleX),
+ ("scaleY", TransformFunction.ScaleY),
+ ("skew", TransformFunction.Skew),
+ ("skewX", TransformFunction.SkewX),
+ ("skewY", TransformFunction.SkewY),
+ ("rotate", TransformFunction.Rotate),
+ ("matrix", TransformFunction.Matrix)
+ };
+
+ private static readonly (string, Unit)[] s_unitMapping =
+ {
+ ("deg", Unit.Degree),
+ ("grad", Unit.Gradian),
+ ("rad", Unit.Radian),
+ ("turn", Unit.Turn),
+ ("px", Unit.Pixel)
+ };
+
+ public static TransformOperations Parse(string s)
+ {
+ void ThrowInvalidFormat()
+ {
+ throw new FormatException($"Invalid transform string: '{s}'.");
+ }
+
+ if (string.IsNullOrEmpty(s))
+ {
+ throw new ArgumentException(nameof(s));
+ }
+
+ var span = s.AsSpan().Trim();
+
+ if (span.Equals("none".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ return TransformOperations.Identity;
+ }
+
+ var builder = TransformOperations.CreateBuilder(0);
+
+ while (true)
+ {
+ var beginIndex = span.IndexOf('(');
+ var endIndex = span.IndexOf(')');
+
+ if (beginIndex == -1 || endIndex == -1)
+ {
+ ThrowInvalidFormat();
+ }
+
+ var namePart = span.Slice(0, beginIndex).Trim();
+
+ var function = ParseTransformFunction(in namePart);
+
+ if (function == TransformFunction.Invalid)
+ {
+ ThrowInvalidFormat();
+ }
+
+ var valuePart = span.Slice(beginIndex + 1, endIndex - beginIndex - 1).Trim();
+
+ ParseFunction(in valuePart, function, in builder);
+
+ span = span.Slice(endIndex + 1);
+
+ if (span.IsWhiteSpace())
+ {
+ break;
+ }
+ }
+
+ return builder.Build();
+ }
+
+ private static void ParseFunction(
+ in ReadOnlySpan functionPart,
+ TransformFunction function,
+ in TransformOperations.Builder builder)
+ {
+ static UnitValue ParseValue(ReadOnlySpan part)
+ {
+ int unitIndex = -1;
+
+ for (int i = 0; i < part.Length; i++)
+ {
+ char c = part[i];
+
+ if (char.IsDigit(c) || c == '-' || c == '.')
+ {
+ continue;
+ }
+
+ unitIndex = i;
+ break;
+ }
+
+ Unit unit = Unit.None;
+
+ if (unitIndex != -1)
+ {
+ var unitPart = part.Slice(unitIndex, part.Length - unitIndex);
+
+ unit = ParseUnit(unitPart);
+
+ part = part.Slice(0, unitIndex);
+ }
+
+ var value = double.Parse(part.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture);
+
+ return new UnitValue(unit, value);
+ }
+
+ static int ParseValuePair(
+ in ReadOnlySpan part,
+ ref UnitValue leftValue,
+ ref UnitValue rightValue)
+ {
+ var commaIndex = part.IndexOf(',');
+
+ if (commaIndex != -1)
+ {
+ var leftPart = part.Slice(0, commaIndex).Trim();
+ var rightPart = part.Slice(commaIndex + 1, part.Length - commaIndex - 1).Trim();
+
+ leftValue = ParseValue(leftPart);
+ rightValue = ParseValue(rightPart);
+
+ return 2;
+ }
+
+ leftValue = ParseValue(part);
+
+ return 1;
+ }
+
+ static int ParseCommaDelimitedValues(ReadOnlySpan part, in Span outValues)
+ {
+ int valueIndex = 0;
+
+ while (true)
+ {
+ if (valueIndex >= outValues.Length)
+ {
+ throw new FormatException("Too many provided values.");
+ }
+
+ var commaIndex = part.IndexOf(',');
+
+ if (commaIndex == -1)
+ {
+ if (!part.IsWhiteSpace())
+ {
+ outValues[valueIndex++] = ParseValue(part);
+ }
+
+ break;
+ }
+
+ var valuePart = part.Slice(0, commaIndex).Trim();
+
+ outValues[valueIndex++] = ParseValue(valuePart);
+
+ part = part.Slice(commaIndex + 1, part.Length - commaIndex - 1);
+ }
+
+ return valueIndex;
+ }
+
+ switch (function)
+ {
+ case TransformFunction.Scale:
+ case TransformFunction.ScaleX:
+ case TransformFunction.ScaleY:
+ {
+ var scaleX = UnitValue.One;
+ var scaleY = UnitValue.One;
+
+ int count = ParseValuePair(functionPart, ref scaleX, ref scaleY);
+
+ if (count != 1 && (function == TransformFunction.ScaleX || function == TransformFunction.ScaleY))
+ {
+ ThrowFormatInvalidValueCount(function, 1);
+ }
+
+ VerifyZeroOrUnit(function, in scaleX, Unit.None);
+ VerifyZeroOrUnit(function, in scaleY, Unit.None);
+
+ if (function == TransformFunction.ScaleY)
+ {
+ scaleY = scaleX;
+ scaleX = UnitValue.One;
+ }
+ else if (function == TransformFunction.Scale && count == 1)
+ {
+ scaleY = scaleX;
+ }
+
+ builder.AppendScale(scaleX.Value, scaleY.Value);
+
+ break;
+ }
+ case TransformFunction.Skew:
+ case TransformFunction.SkewX:
+ case TransformFunction.SkewY:
+ {
+ var skewX = UnitValue.Zero;
+ var skewY = UnitValue.Zero;
+
+ int count = ParseValuePair(functionPart, ref skewX, ref skewY);
+
+ if (count != 1 && (function == TransformFunction.SkewX || function == TransformFunction.SkewY))
+ {
+ ThrowFormatInvalidValueCount(function, 1);
+ }
+
+ VerifyZeroOrAngle(function, in skewX);
+ VerifyZeroOrAngle(function, in skewY);
+
+ if (function == TransformFunction.SkewY)
+ {
+ skewY = skewX;
+ skewX = UnitValue.Zero;
+ }
+
+ builder.AppendSkew(ToRadians(in skewX), ToRadians(in skewY));
+
+ break;
+ }
+ case TransformFunction.Rotate:
+ {
+ var angle = UnitValue.Zero;
+ UnitValue _ = default;
+
+ int count = ParseValuePair(functionPart, ref angle, ref _);
+
+ if (count != 1)
+ {
+ ThrowFormatInvalidValueCount(function, 1);
+ }
+
+ VerifyZeroOrAngle(function, in angle);
+
+ builder.AppendRotate(ToRadians(in angle));
+
+ break;
+ }
+ case TransformFunction.Translate:
+ case TransformFunction.TranslateX:
+ case TransformFunction.TranslateY:
+ {
+ var translateX = UnitValue.Zero;
+ var translateY = UnitValue.Zero;
+
+ int count = ParseValuePair(functionPart, ref translateX, ref translateY);
+
+ if (count != 1 && (function == TransformFunction.TranslateX || function == TransformFunction.TranslateY))
+ {
+ ThrowFormatInvalidValueCount(function, 1);
+ }
+
+ VerifyZeroOrUnit(function, in translateX, Unit.Pixel);
+ VerifyZeroOrUnit(function, in translateY, Unit.Pixel);
+
+ if (function == TransformFunction.TranslateY)
+ {
+ translateY = translateX;
+ translateX = UnitValue.Zero;
+ }
+
+ builder.AppendTranslate(translateX.Value, translateY.Value);
+
+ break;
+ }
+ case TransformFunction.Matrix:
+ {
+ Span values = stackalloc UnitValue[6];
+
+ int count = ParseCommaDelimitedValues(functionPart, in values);
+
+ if (count != 6)
+ {
+ ThrowFormatInvalidValueCount(function, 6);
+ }
+
+ foreach (UnitValue value in values)
+ {
+ VerifyZeroOrUnit(function, value, Unit.None);
+ }
+
+ var matrix = new Matrix(
+ values[0].Value,
+ values[1].Value,
+ values[2].Value,
+ values[3].Value,
+ values[4].Value,
+ values[5].Value);
+
+ builder.AppendMatrix(matrix);
+
+ break;
+ }
+ }
+ }
+
+ private static void VerifyZeroOrUnit(TransformFunction function, in UnitValue value, Unit unit)
+ {
+ bool isZero = value.Unit == Unit.None && value.Value == 0d;
+
+ if (!isZero && value.Unit != unit)
+ {
+ ThrowFormatInvalidValue(function, in value);
+ }
+ }
+
+ private static void VerifyZeroOrAngle(TransformFunction function, in UnitValue value)
+ {
+ if (value.Value != 0d && !IsAngleUnit(value.Unit))
+ {
+ ThrowFormatInvalidValue(function, in value);
+ }
+ }
+
+ private static bool IsAngleUnit(Unit unit)
+ {
+ switch (unit)
+ {
+ case Unit.Radian:
+ case Unit.Gradian:
+ case Unit.Degree:
+ case Unit.Turn:
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static void ThrowFormatInvalidValue(TransformFunction function, in UnitValue value)
+ {
+ var unitString = value.Unit == Unit.None ? string.Empty : value.Unit.ToString();
+
+ throw new FormatException($"Invalid value {value.Value} {unitString} for {function}");
+ }
+
+ private static void ThrowFormatInvalidValueCount(TransformFunction function, int count)
+ {
+ throw new FormatException($"Invalid format. {function} expects {count} value(s).");
+ }
+
+ private static Unit ParseUnit(in ReadOnlySpan part)
+ {
+ foreach (var (name, unit) in s_unitMapping)
+ {
+ if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ return unit;
+ }
+ }
+
+ throw new FormatException($"Invalid unit: {part.ToString()}");
+ }
+
+ private static TransformFunction ParseTransformFunction(in ReadOnlySpan part)
+ {
+ foreach (var (name, transformFunction) in s_functionMapping)
+ {
+ if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ return transformFunction;
+ }
+ }
+
+ return TransformFunction.Invalid;
+ }
+
+ private static double ToRadians(in UnitValue value)
+ {
+ return value.Unit switch
+ {
+ Unit.Radian => value.Value,
+ Unit.Gradian => MathUtilities.Grad2Rad(value.Value),
+ Unit.Degree => MathUtilities.Deg2Rad(value.Value),
+ Unit.Turn => MathUtilities.Turn2Rad(value.Value),
+ _ => value.Value
+ };
+ }
+
+ private enum Unit
+ {
+ None,
+ Pixel,
+ Radian,
+ Gradian,
+ Degree,
+ Turn
+ }
+
+ private readonly struct UnitValue
+ {
+ public readonly Unit Unit;
+ public readonly double Value;
+
+ public UnitValue(Unit unit, double value)
+ {
+ Unit = unit;
+ Value = value;
+ }
+
+ public static UnitValue Zero => new UnitValue(Unit.None, 0);
+
+ public static UnitValue One => new UnitValue(Unit.None, 1);
+ }
+
+ private enum TransformFunction
+ {
+ Invalid,
+ Translate,
+ TranslateX,
+ TranslateY,
+ Scale,
+ ScaleX,
+ ScaleY,
+ Skew,
+ SkewX,
+ SkewY,
+ Rotate,
+ Matrix
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs
index 6cd6442095..5d802c27b9 100644
--- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs
@@ -6,6 +6,7 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Transformation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs
index 0383094a5b..f87b336b50 100644
--- a/src/Avalonia.Visuals/Size.cs
+++ b/src/Avalonia.Visuals/Size.cs
@@ -189,7 +189,7 @@ namespace Avalonia
}
///
- /// Returns a boolean indicating whether the size is equal to the other given size.
+ /// Returns a boolean indicating whether the size is equal to the other given size (bitwise).
///
/// The other size to test equality against.
/// True if this size is equal to other; False otherwise.
@@ -201,6 +201,17 @@ namespace Avalonia
// ReSharper enable CompareOfFloatsByEqualityOperator
}
+ ///
+ /// Returns a boolean indicating whether the size is equal to the other given size (numerically).
+ ///
+ /// The other size to test equality against.
+ /// True if this size is equal to other; False otherwise.
+ public bool NearlyEquals(Size other)
+ {
+ return MathUtilities.AreClose(_width, other._width) &&
+ MathUtilities.AreClose(_height, other._height);
+ }
+
///
/// Checks for equality between a size and an object.
///
diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs
index 2b5d79173b..6059dc3971 100644
--- a/src/Avalonia.Visuals/Vector.cs
+++ b/src/Avalonia.Visuals/Vector.cs
@@ -2,7 +2,8 @@ using System;
using System.Globalization;
using Avalonia.Animation.Animators;
using Avalonia.Utilities;
-using JetBrains.Annotations;
+
+#nullable enable
namespace Avalonia
{
@@ -17,20 +18,20 @@ namespace Avalonia
}
///
- /// The X vector.
+ /// The X component.
///
private readonly double _x;
///
- /// The Y vector.
+ /// The Y component.
///
private readonly double _y;
///
/// Initializes a new instance of the structure.
///
- /// The X vector.
- /// The Y vector.
+ /// The X component.
+ /// The Y component.
public Vector(double x, double y)
{
_x = x;
@@ -38,12 +39,12 @@ namespace Avalonia
}
///
- /// Gets the X vector.
+ /// Gets the X component.
///
public double X => _x;
///
- /// Gets the Y vector.
+ /// Gets the Y component.
///
public double Y => _y;
@@ -57,18 +58,18 @@ namespace Avalonia
}
///
- /// Calculates the dot product of two vectors
+ /// Calculates the dot product of two vectors.
///
- /// First vector
- /// Second vector
- /// The dot product
+ /// First vector.
+ /// Second vector.
+ /// The dot product.
public static double operator *(Vector a, Vector b)
=> Dot(a, b);
///
/// Scales a vector.
///
- /// The vector
+ /// The vector.
/// The scaling factor.
/// The scaled vector.
public static Vector operator *(Vector vector, double scale)
@@ -77,7 +78,7 @@ namespace Avalonia
///
/// Scales a vector.
///
- /// The vector
+ /// The vector.
/// The divisor.
/// The scaled vector.
public static Vector operator /(Vector vector, double scale)
@@ -100,12 +101,12 @@ namespace Avalonia
}
///
- /// Length of the vector
+ /// Length of the vector.
///
public double Length => Math.Sqrt(SquaredLength);
///
- /// Squared Length of the vector
+ /// Squared Length of the vector.
///
public double SquaredLength => _x * _x + _y * _y;
@@ -154,9 +155,8 @@ namespace Avalonia
/// True if vectors are nearly equal.
public bool NearlyEquals(Vector other)
{
- const float tolerance = float.Epsilon;
-
- return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance;
+ return MathUtilities.AreClose(_x, other._x) &&
+ MathUtilities.AreClose(_y, other._y);
}
public override bool Equals(object obj) => obj is Vector other && Equals(other);
@@ -189,9 +189,9 @@ namespace Avalonia
}
///
- /// Returns a new vector with the specified X coordinate.
+ /// Returns a new vector with the specified X component.
///
- /// The X coordinate.
+ /// The X component.
/// The new vector.
public Vector WithX(double x)
{
@@ -199,9 +199,9 @@ namespace Avalonia
}
///
- /// Returns a new vector with the specified Y coordinate.
+ /// Returns a new vector with the specified Y component.
///
- /// The Y coordinate.
+ /// The Y component.
/// The new vector.
public Vector WithY(double y)
{
@@ -311,25 +311,25 @@ namespace Avalonia
=> new Vector(-vector._x, -vector._y);
///
- /// Returnes the vector (0.0, 0.0)
+ /// Returns the vector (0.0, 0.0).
///
public static Vector Zero
=> new Vector(0, 0);
///
- /// Returnes the vector (1.0, 1.0)
+ /// Returns the vector (1.0, 1.0).
///
public static Vector One
=> new Vector(1, 1);
///
- /// Returnes the vector (1.0, 0.0)
+ /// Returns the vector (1.0, 0.0).
///
public static Vector UnitX
=> new Vector(1, 0);
///
- /// Returnes the vector (0.0, 1.0)
+ /// Returns the vector (0.0, 1.0).
///
public static Vector UnitY
=> new Vector(0, 1);
diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs
index bb9a4cf208..cd6e5bb075 100644
--- a/src/Avalonia.Visuals/Visual.cs
+++ b/src/Avalonia.Visuals/Visual.cs
@@ -68,8 +68,8 @@ namespace Avalonia
///
/// Defines the property.
///
- public static readonly StyledProperty RenderTransformProperty =
- AvaloniaProperty.Register(nameof(RenderTransform));
+ public static readonly StyledProperty RenderTransformProperty =
+ AvaloniaProperty.Register(nameof(RenderTransform));
///
/// Defines the property.
@@ -219,7 +219,7 @@ namespace Avalonia
///
/// Gets the render transform of the control.
///
- public Transform RenderTransform
+ public ITransform RenderTransform
{
get { return GetValue(RenderTransformProperty); }
set { SetValue(RenderTransformProperty, value); }
@@ -391,9 +391,9 @@ namespace Avalonia
_visualRoot = e.Root;
- if (RenderTransform != null)
+ if (RenderTransform is IMutableTransform mutableTransform)
{
- RenderTransform.Changed += RenderTransformChanged;
+ mutableTransform.Changed += RenderTransformChanged;
}
EnableTransitions();
@@ -428,9 +428,9 @@ namespace Avalonia
_visualRoot = null;
- if (RenderTransform != null)
+ if (RenderTransform is IMutableTransform mutableTransform)
{
- RenderTransform.Changed -= RenderTransformChanged;
+ mutableTransform.Changed -= RenderTransformChanged;
}
DisableTransitions();
diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs
index 6f905cc269..50787655d9 100644
--- a/src/Avalonia.Visuals/VisualTree/IVisual.cs
+++ b/src/Avalonia.Visuals/VisualTree/IVisual.cs
@@ -76,7 +76,7 @@ namespace Avalonia.VisualTree
///
/// Gets or sets the render transform of the control.
///
- Transform RenderTransform { get; set; }
+ ITransform RenderTransform { get; set; }
///
/// Gets or sets the render transform origin of the control.
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 1d41fe4bdd..90064cb28d 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -922,7 +922,7 @@ namespace Avalonia.X11
public IScreenImpl Screen => _platform.Screens;
- public Size MaxClientSize => _platform.X11Screens.Screens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
+ public Size MaxAutoSizeHint => _platform.X11Screens.Screens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
.OrderByDescending(x => x.Width + x.Height).FirstOrDefault();
diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs
index 0734532d92..4e44f55fe0 100644
--- a/src/Avalonia.X11/XI2Manager.cs
+++ b/src/Avalonia.X11/XI2Manager.cs
@@ -97,7 +97,7 @@ namespace Avalonia.X11
{
_platform = platform;
_x11 = platform.Info;
- _multitouch = platform.Options?.EnableMultiTouch ?? false;
+ _multitouch = platform.Options?.EnableMultiTouch ?? true;
var devices =(XIDeviceInfo*) XIQueryDevice(_x11.Display,
(int)XiPredefinedDeviceId.XIAllMasterDevices, out int num);
for (var c = 0; c < num; c++)
@@ -237,6 +237,22 @@ namespace Avalonia.X11
RawPointerEventType.Move, ev.Position, ev.Modifiers));
}
+ if (ev.Type == XiEventType.XI_ButtonPress && ev.Button >= 4 && ev.Button <= 7 && !ev.Emulated)
+ {
+ Vector? scrollDelta = ev.Button switch
+ {
+ 4 => new Vector(0, 1),
+ 5 => new Vector(0, -1),
+ 6 => new Vector(1, 0),
+ 7 => new Vector(-1, 0),
+ _ => null
+ };
+
+ if (scrollDelta.HasValue)
+ client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
+ client.InputRoot, ev.Position, scrollDelta.Value, ev.Modifiers));
+ }
+
if (ev.Type == XiEventType.XI_ButtonPress || ev.Type == XiEventType.XI_ButtonRelease)
{
var down = ev.Type == XiEventType.XI_ButtonPress;
diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs
index 9ab5882361..7aa1eed890 100644
--- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs
+++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs
@@ -107,6 +107,14 @@ namespace Avalonia.Data
private object ConvertValue(IList values, Type targetType, IMultiValueConverter converter)
{
+ for (var i = 0; i < values.Count; ++i)
+ {
+ if (values[i] is BindingNotification notification)
+ {
+ values[i] = notification.Value;
+ }
+ }
+
var culture = CultureInfo.CurrentCulture;
var converted = converter.Convert(values, targetType, ConverterParameter, culture);
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
index 696dc5978e..e0de40525f 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
@@ -236,7 +236,7 @@ namespace Avalonia.Direct2D1.Media
Math.Max(rrect.RadiiTopRight.X, Math.Max(rrect.RadiiBottomRight.X, rrect.RadiiBottomLeft.X)));
var radiusY = Math.Max(rrect.RadiiTopLeft.Y,
Math.Max(rrect.RadiiTopRight.Y, Math.Max(rrect.RadiiBottomRight.Y, rrect.RadiiBottomLeft.Y)));
- var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon;
+ var isRounded = !MathUtilities.IsZero(radiusX) || !MathUtilities.IsZero(radiusY);
if (brush != null)
{
diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs
index efcf1ea674..7f27a9e841 100644
--- a/src/Windows/Avalonia.Win32/PopupImpl.cs
+++ b/src/Windows/Avalonia.Win32/PopupImpl.cs
@@ -8,12 +8,35 @@ namespace Avalonia.Win32
class PopupImpl : WindowImpl, IPopupImpl
{
private bool _dropShadowHint = true;
+ private Size? _maxAutoSize;
public override void Show()
{
UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate);
}
+ public override Size MaxAutoSizeHint
+ {
+ get
+ {
+ if (_maxAutoSize is null)
+ {
+ var monitor = UnmanagedMethods.MonitorFromWindow(
+ Hwnd,
+ UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST);
+
+ if (monitor != IntPtr.Zero)
+ {
+ var info = UnmanagedMethods.MONITORINFO.Create();
+ UnmanagedMethods.GetMonitorInfo(monitor, ref info);
+ _maxAutoSize = info.rcWork.ToPixelRect().ToRect(Scaling).Size;
+ }
+ }
+
+ return _maxAutoSize ?? Size.Infinity;
+ }
+ }
+
protected override IntPtr CreateWindowOverride(ushort atom)
{
UnmanagedMethods.WindowStyles style =
@@ -47,6 +70,9 @@ namespace Avalonia.Win32
{
switch ((UnmanagedMethods.WindowsMessage)msg)
{
+ case UnmanagedMethods.WindowsMessage.WM_DISPLAYCHANGE:
+ _maxAutoSize = null;
+ goto default;
case UnmanagedMethods.WindowsMessage.WM_MOUSEACTIVATE:
return (IntPtr)UnmanagedMethods.MouseActivate.MA_NOACTIVATE;
default:
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs
index 138553b962..50e71aeebe 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs
@@ -393,6 +393,8 @@ namespace Avalonia.Win32
case WindowsMessage.WM_GETMINMAXINFO:
{
MINMAXINFO mmi = Marshal.PtrToStructure(lParam);
+
+ _maxTrackSize = mmi.ptMaxTrackSize;
if (_minSize.Width > 0)
{
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 0cf5a73b9f..6024dc01da 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -66,6 +66,7 @@ namespace Avalonia.Win32
private OleDropTarget _dropTarget;
private Size _minSize;
private Size _maxSize;
+ private POINT _maxTrackSize;
private WindowImpl _parent;
public WindowImpl()
@@ -168,16 +169,7 @@ namespace Avalonia.Win32
public IPlatformHandle Handle { get; private set; }
- public Size MaxClientSize
- {
- get
- {
- return (new Size(
- GetSystemMetrics(SystemMetric.SM_CXMAXTRACK),
- GetSystemMetrics(SystemMetric.SM_CYMAXTRACK))
- - BorderThickness) / Scaling;
- }
- }
+ public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / Scaling, _maxTrackSize.Y / Scaling);
public IMouseDevice MouseDevice => _mouseDevice;
@@ -211,6 +203,8 @@ namespace Avalonia.Win32
public WindowTransparencyLevel TransparencyLevel { get; private set; }
+ protected IntPtr Hwnd => _hwnd;
+
public void SetTransparencyLevelHint (WindowTransparencyLevel transparencyLevel)
{
TransparencyLevel = EnableBlur(transparencyLevel);
@@ -577,7 +571,7 @@ namespace Avalonia.Win32
Handle = new PlatformHandle(_hwnd, PlatformConstants.WindowHandleType);
- _multitouch = Win32Platform.Options.EnableMultitouch ?? false;
+ _multitouch = Win32Platform.Options.EnableMultitouch ?? true;
if (_multitouch)
{
diff --git a/tests/Avalonia.Animation.UnitTests/TestClock.cs b/tests/Avalonia.Animation.UnitTests/TestClock.cs
index a1c4ff9277..4812880c03 100644
--- a/tests/Avalonia.Animation.UnitTests/TestClock.cs
+++ b/tests/Avalonia.Animation.UnitTests/TestClock.cs
@@ -5,10 +5,12 @@ namespace Avalonia.Animation.UnitTests
{
internal class TestClock : IClock, IDisposable
{
+ private TimeSpan _curTime;
+
private IObserver _observer;
public PlayState PlayState { get; set; } = PlayState.Run;
-
+
public void Dispose()
{
_observer?.OnCompleted();
@@ -19,6 +21,12 @@ namespace Avalonia.Animation.UnitTests
_observer?.OnNext(time);
}
+ public void Pulse(TimeSpan time)
+ {
+ _curTime += time;
+ _observer?.OnNext(_curTime);
+ }
+
public IDisposable Subscribe(IObserver observer)
{
_observer = observer;
diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs
index 70ffd781a1..640013dedd 100644
--- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs
+++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs
@@ -10,13 +10,11 @@ namespace Avalonia.Animation.UnitTests
[Fact]
public void Check_Transitions_Interpolation_Negative_Bounds_Clamp()
{
- var clock = new MockGlobalClock();
+ var clock = new TestClock();
- using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
+ var border = new Border
{
- var border = new Border
- {
- Transitions = new Transitions
+ Transitions = new Transitions
{
new DoubleTransition
{
@@ -24,27 +22,25 @@ namespace Avalonia.Animation.UnitTests
Property = Border.OpacityProperty,
}
}
- };
+ };
- border.Opacity = 0;
+ border.Opacity = 0;
- clock.Pulse(TimeSpan.FromSeconds(0));
- clock.Pulse(TimeSpan.FromSeconds(-0.5));
+ clock.Pulse(TimeSpan.FromSeconds(0));
+ clock.Pulse(TimeSpan.FromSeconds(-0.5));
+
+ Assert.Equal(0, border.Opacity);
- Assert.Equal(0, border.Opacity);
- }
}
[Fact]
public void Check_Transitions_Interpolation_Positive_Bounds_Clamp()
{
- var clock = new MockGlobalClock();
+ var clock = new TestClock();
- using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
+ var border = new Border
{
- var border = new Border
- {
- Transitions = new Transitions
+ Transitions = new Transitions
{
new DoubleTransition
{
@@ -52,34 +48,62 @@ namespace Avalonia.Animation.UnitTests
Property = Border.OpacityProperty,
}
}
- };
+ };
- border.Opacity = 0;
+ border.Opacity = 0;
- clock.Pulse(TimeSpan.FromSeconds(0));
- clock.Pulse(TimeSpan.FromMilliseconds(1001));
+ clock.Pulse(TimeSpan.FromSeconds(0));
+ clock.Pulse(TimeSpan.FromMilliseconds(1001));
+
+ Assert.Equal(0, border.Opacity);
- Assert.Equal(0, border.Opacity);
- }
}
[Fact]
public void TransitionInstance_With_Zero_Duration_Is_Completed_On_First_Tick()
{
- var clock = new MockGlobalClock();
+ var clock = new TestClock();
- using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
+ int i = 0;
+ var inst = new TransitionInstance(clock, TimeSpan.Zero, TimeSpan.Zero).Subscribe(nextValue =>
{
- int i = 0;
- var inst = new TransitionInstance(clock, TimeSpan.Zero).Subscribe(nextValue =>
+ switch (i++)
{
- switch (i++)
- {
- case 0: Assert.Equal(0, nextValue); break;
- case 1: Assert.Equal(1d, nextValue); break;
- }
- });
+ case 0: Assert.Equal(0, nextValue); break;
+ case 1: Assert.Equal(1d, nextValue); break;
+ }
+ });
+
+ clock.Pulse(TimeSpan.FromMilliseconds(10));
+ }
+
+ [Fact]
+ public void TransitionInstance_Properly_Calculates_Delay_And_Duration_Values()
+ {
+ var clock = new TestClock();
+
+ int i = -1;
+ var inst = new TransitionInstance(clock, TimeSpan.FromMilliseconds(30), TimeSpan.FromMilliseconds(70)).Subscribe(nextValue =>
+ {
+ switch (i++)
+ {
+ case 0: Assert.Equal(0, nextValue); break;
+ case 1: Assert.Equal(0, nextValue); break;
+ case 2: Assert.Equal(0, nextValue); break;
+ case 3: Assert.Equal(0, nextValue); break;
+ case 4: Assert.Equal(Math.Round(10d / 70d, 4), Math.Round(nextValue, 4)); break;
+ case 5: Assert.Equal(Math.Round(20d / 70d, 4), Math.Round(nextValue, 4)); break;
+ case 6: Assert.Equal(Math.Round(30d / 70d, 4), Math.Round(nextValue, 4)); break;
+ case 7: Assert.Equal(Math.Round(40d / 70d, 4), Math.Round(nextValue, 4)); break;
+ case 8: Assert.Equal(Math.Round(50d / 70d, 4), Math.Round(nextValue, 4)); break;
+ case 9: Assert.Equal(Math.Round(60d / 70d, 4), Math.Round(nextValue, 4)); break;
+ case 10: Assert.Equal(1d, nextValue); break;
+ }
+ });
+
+ for (int z = 0; z <= 10; z++)
+ {
clock.Pulse(TimeSpan.FromMilliseconds(10));
}
}
diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
index 6e175707e4..df871a67b4 100644
--- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
+++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
@@ -322,7 +322,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
{
"bar",
new BindingNotification(
- new MissingMemberException("Could not find CLR property 'Bar' on 'Avalonia.Base.UnitTests.Data.Core.ExpressionObserverTests_Property+WithoutBar'"),
+ new MissingMemberException("Could not find a matching property accessor for 'Bar' on 'Avalonia.Base.UnitTests.Data.Core.ExpressionObserverTests_Property+WithoutBar'"),
BindingErrorType.Error),
"baz",
},
diff --git a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs
new file mode 100644
index 0000000000..a12d07b8ef
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs
@@ -0,0 +1,222 @@
+using System;
+using Avalonia.Utilities;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Utilities
+{
+ public class MathUtilitiesTests
+ {
+ private const double AnyValue = 42.42;
+ private readonly double _calculatedAnyValue;
+ private readonly double _one;
+ private readonly double _zero;
+
+ public MathUtilitiesTests()
+ {
+ _calculatedAnyValue = 0.0;
+ _one = 0.0;
+ _zero = 1.0;
+
+ const int N = 10;
+ var dxAny = AnyValue / N;
+ var dxOne = 1.0 / N;
+ var dxZero = _zero / N;
+
+ for (var i = 0; i < N; ++i)
+ {
+ _calculatedAnyValue += dxAny;
+ _one += dxOne;
+ _zero -= dxZero;
+ }
+ }
+
+ [Fact]
+ public void Two_Equivalent_Double_Values_Are_Close()
+ {
+ var actual = MathUtilities.AreClose(AnyValue, _calculatedAnyValue);
+
+ Assert.True(actual);
+ Assert.Equal(AnyValue, Math.Round(_calculatedAnyValue, 14));
+ }
+
+ [Fact]
+ public void Two_Equivalent_Single_Values_Are_Close()
+ {
+ var expectedValue = (float)AnyValue;
+ var actualValue = (float)_calculatedAnyValue;
+
+ var actual = MathUtilities.AreClose(expectedValue, actualValue);
+
+ Assert.True(actual);
+ Assert.Equal((float) Math.Round(expectedValue, 5), (float) Math.Round(actualValue, 4));
+ }
+
+ [Fact]
+ public void Calculated_Double_One_Is_One()
+ {
+ var actual = MathUtilities.IsOne(_one);
+
+ Assert.True(actual);
+ Assert.Equal(1.0, Math.Round(_one, 15));
+ }
+
+ [Fact]
+ public void Calculated_Single_One_Is_One()
+ {
+ var actualValue = (float)_one;
+
+ var actual = MathUtilities.IsOne(actualValue);
+
+ Assert.True(actual);
+ Assert.Equal(1.0f, (float) Math.Round(actualValue, 7));
+ }
+
+ [Fact]
+ public void Calculated_Double_Zero_Is_Zero()
+ {
+ var actual = MathUtilities.IsZero(_zero);
+
+ Assert.True(actual);
+ Assert.Equal(0.0, Math.Round(_zero, 15));
+ }
+
+ [Fact]
+ public void Calculated_Single_Zero_Is_Zero()
+ {
+ var actualValue = (float)_zero;
+
+ var actual = MathUtilities.IsZero(actualValue);
+
+ Assert.True(actual);
+ Assert.Equal(0.0f, (float) Math.Round(actualValue, 7));
+ }
+
+ [Fact]
+ public void Float_Clamp_Input_NaN_Return_NaN()
+ {
+ var clamp = MathUtilities.Clamp(double.NaN, 0.0, 1.0);
+ Assert.True(double.IsNaN(clamp));
+ }
+
+ [Fact]
+ public void Float_Clamp_Input_NegativeInfinity_Return_Min()
+ {
+ const double min = 0.0;
+ const double max = 1.0;
+ var actual = MathUtilities.Clamp(double.NegativeInfinity, min, max);
+ Assert.Equal(min, actual);
+ }
+
+ [Fact]
+ public void Float_Clamp_Input_PositiveInfinity_Return_Max()
+ {
+ const double min = 0.0;
+ const double max = 1.0;
+ var actual = MathUtilities.Clamp(double.PositiveInfinity, min, max);
+ Assert.Equal(max, actual);
+ }
+
+ [Fact]
+ public void Double_Float_Zero_Less_Than_One()
+ {
+ var actual = MathUtilities.LessThan(0d, 1d);
+ Assert.True(actual);
+ }
+
+ [Fact]
+ public void Single_Float_Zero_Less_Than_One()
+ {
+ var actual = MathUtilities.LessThan(0f, 1f);
+ Assert.True(actual);
+ }
+
+ [Fact]
+ public void Double_Float_One_Not_Less_Than_Zero()
+ {
+ var actual = MathUtilities.LessThan(1d, 0d);
+ Assert.False(actual);
+ }
+
+ [Fact]
+ public void Single_Float_One_Not_Less_Than_Zero()
+ {
+ var actual = MathUtilities.LessThan(1f, 0f);
+ Assert.False(actual);
+ }
+
+ [Fact]
+ public void Double_Float_Zero_Not_Greater_Than_One()
+ {
+ var actual = MathUtilities.GreaterThan(0d, 1d);
+ Assert.False(actual);
+ }
+
+ [Fact]
+ public void Single_Float_Zero_Not_Greater_Than_One()
+ {
+ var actual = MathUtilities.GreaterThan(0f, 1f);
+ Assert.False(actual);
+ }
+
+ [Fact]
+ public void Double_Float_One_Greater_Than_Zero()
+ {
+ var actual = MathUtilities.GreaterThan(1d, 0d);
+ Assert.True(actual);
+ }
+
+ [Fact]
+ public void Single_Float_One_Greater_Than_Zero()
+ {
+ var actual = MathUtilities.GreaterThan(1f, 0f);
+ Assert.True(actual);
+ }
+
+ [Fact]
+ public void Double_Float_One_Less_Than_Or_Close_One()
+ {
+ var actual = MathUtilities.LessThanOrClose(1d, 1d);
+ Assert.True(actual);
+ }
+
+ [Fact]
+ public void Single_Float_One_Less_Than_Or_Close_One()
+ {
+ var actual = MathUtilities.LessThanOrClose(1f, 1f);
+ Assert.True(actual);
+ }
+
+ [Fact]
+ public void Double_Float_One_Greater_Than_Or_Close_One()
+ {
+ var actual = MathUtilities.GreaterThanOrClose(1d, 1d);
+ Assert.True(actual);
+ }
+
+ [Fact]
+ public void Single_Float_One_Greater_Than_Or_Close_One()
+ {
+ var actual = MathUtilities.GreaterThanOrClose(1f, 1f);
+ Assert.True(actual);
+ }
+
+ [Fact]
+ public void Round_Layout_Value_Without_DPI_Aware()
+ {
+ const double value = 42.5;
+ var expectedValue = Math.Round(value);
+ var actualValue = MathUtilities.RoundLayoutValue(value, 1.0);
+ Assert.Equal(expectedValue, actualValue);
+ }
+
+ [Fact]
+ public void Round_Layout_Value_With_DPI_Aware()
+ {
+ const double dpiScale = 1.25;
+ const double value = 42.5;
+ var expectedValue = Math.Round(value * dpiScale) / dpiScale;
+ var actualValue = MathUtilities.RoundLayoutValue(value, dpiScale);
+ Assert.Equal(expectedValue, actualValue);
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs b/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs
new file mode 100644
index 0000000000..0039f5670c
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs
@@ -0,0 +1,47 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using JetBrains.Annotations;
+
+namespace Avalonia.Benchmarks.Data
+{
+ internal class AccessorTestObject : INotifyPropertyChanged
+ {
+ private string _test;
+
+ public string Test
+ {
+ get => _test;
+ set
+ {
+ if (_test == value)
+ {
+ return;
+ }
+
+ _test = value;
+
+ OnPropertyChanged();
+ }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public void Execute()
+ {
+ }
+
+ public void Execute(object p0)
+ {
+ }
+
+ public void Execute(object p0, object p1)
+ {
+ }
+
+ [NotifyPropertyChangedInvocator]
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs b/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs
index 3d5a4029bb..6c5e0d3b53 100644
--- a/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs
+++ b/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs
@@ -1,9 +1,6 @@
using System;
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
using Avalonia.Data.Core.Plugins;
using BenchmarkDotNet.Attributes;
-using JetBrains.Annotations;
namespace Avalonia.Benchmarks.Data
{
@@ -12,7 +9,7 @@ namespace Avalonia.Benchmarks.Data
{
private readonly InpcPropertyAccessorPlugin _inpcPlugin = new InpcPropertyAccessorPlugin();
private readonly MethodAccessorPlugin _methodPlugin = new MethodAccessorPlugin();
- private readonly TestObject _targetStrongRef = new TestObject();
+ private readonly AccessorTestObject _targetStrongRef = new AccessorTestObject();
private readonly WeakReference _targetWeakRef;
public PropertyAccessorBenchmarks()
@@ -23,66 +20,25 @@ namespace Avalonia.Benchmarks.Data
[Benchmark]
public void InpcAccessorMatch()
{
- _inpcPlugin.Match(_targetWeakRef, nameof(TestObject.Test));
+ _inpcPlugin.Match(_targetWeakRef, nameof(AccessorTestObject.Test));
}
[Benchmark]
public void InpcAccessorStart()
{
- _inpcPlugin.Start(_targetWeakRef, nameof(TestObject.Test));
+ _inpcPlugin.Start(_targetWeakRef, nameof(AccessorTestObject.Test));
}
[Benchmark]
public void MethodAccessorMatch()
{
- _methodPlugin.Match(_targetWeakRef, nameof(TestObject.Execute));
+ _methodPlugin.Match(_targetWeakRef, nameof(AccessorTestObject.Execute));
}
[Benchmark]
public void MethodAccessorStart()
{
- _methodPlugin.Start(_targetWeakRef, nameof(TestObject.Execute));
- }
-
- private class TestObject : INotifyPropertyChanged
- {
- private string _test;
-
- public string Test
- {
- get => _test;
- set
- {
- if (_test == value)
- {
- return;
- }
-
- _test = value;
-
- OnPropertyChanged();
- }
- }
-
- public void Execute()
- {
- }
-
- public void Execute(object p0)
- {
- }
-
- public void Execute(object p0, object p1)
- {
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- [NotifyPropertyChangedInvocator]
- protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
+ _methodPlugin.Start(_targetWeakRef, nameof(AccessorTestObject.Execute));
}
}
}
diff --git a/tests/Avalonia.Benchmarks/Data/PropertyAccessorPluginBenchmarks.cs b/tests/Avalonia.Benchmarks/Data/PropertyAccessorPluginBenchmarks.cs
new file mode 100644
index 0000000000..75e3269e3d
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Data/PropertyAccessorPluginBenchmarks.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using Avalonia.Data.Core.Plugins;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Data
+{
+ [MemoryDiagnoser, InProcess]
+ public class PropertyAccessorPluginBenchmarks
+ {
+ private readonly AccessorTestObject _targetStrongRef = new AccessorTestObject();
+
+ private readonly List _oldPlugins;
+ private readonly List _newPlugins;
+
+ public PropertyAccessorPluginBenchmarks()
+ {
+ _oldPlugins = new List
+ {
+ new AvaloniaPropertyAccessorPlugin(),
+ new MethodAccessorPlugin(),
+ new InpcPropertyAccessorPlugin()
+ };
+
+ _newPlugins = new List
+ {
+ new AvaloniaPropertyAccessorPlugin(),
+ new InpcPropertyAccessorPlugin(),
+ new MethodAccessorPlugin()
+ };
+ }
+
+ [Benchmark]
+ public void MatchAccessorOld()
+ {
+ var propertyName = nameof(AccessorTestObject.Test);
+
+ foreach (IPropertyAccessorPlugin x in _oldPlugins)
+ {
+ if (x.Match(_targetStrongRef, propertyName))
+ {
+ break;
+ }
+ }
+ }
+
+ [Benchmark]
+ public void MatchAccessorNew()
+ {
+ var propertyName = nameof(AccessorTestObject.Test);
+
+ foreach (IPropertyAccessorPlugin x in _newPlugins)
+ {
+ if (x.Match(_targetStrongRef, propertyName))
+ {
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs b/tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs
new file mode 100644
index 0000000000..17e2237eb0
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs
@@ -0,0 +1,16 @@
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Visuals
+{
+ [MemoryDiagnoser, InProcess]
+ public class MatrixBenchmarks
+ {
+ private static readonly Matrix s_data = Matrix.Identity;
+
+ [Benchmark(Baseline = true)]
+ public bool Decompose()
+ {
+ return Matrix.TryDecomposeTransform(s_data, out Matrix.Decomposed decomposed);
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs
similarity index 90%
rename from tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
rename to tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs
index 3d396a9726..f41a3e7581 100644
--- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs
@@ -15,7 +15,7 @@ using Xunit;
namespace Avalonia.Controls.UnitTests
{
- public class DatePickerTests
+ public class CalendarDatePickerTests
{
private static bool CompareDates(DateTime first, DateTime second)
{
@@ -30,7 +30,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(Services))
{
bool handled = false;
- DatePicker datePicker = CreateControl();
+ CalendarDatePicker datePicker = CreateControl();
datePicker.SelectedDateChanged += (s,e) =>
{
handled = true;
@@ -47,7 +47,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(Services))
{
- DatePicker datePicker = CreateControl();
+ CalendarDatePicker datePicker = CreateControl();
datePicker.BlackoutDates.AddDatesInPast();
DateTime goodValue = DateTime.Today.AddDays(1);
@@ -65,7 +65,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(Services))
{
- DatePicker datePicker = CreateControl();
+ CalendarDatePicker datePicker = CreateControl();
datePicker.SelectedDate = DateTime.Today.AddDays(5);
Assert.ThrowsAny(
@@ -76,10 +76,10 @@ namespace Avalonia.Controls.UnitTests
private static TestServices Services => TestServices.MockThreadingInterface.With(
standardCursorFactory: Mock.Of());
- private DatePicker CreateControl()
+ private CalendarDatePicker CreateControl()
{
var datePicker =
- new DatePicker
+ new CalendarDatePicker
{
Template = CreateTemplate()
};
@@ -90,7 +90,7 @@ namespace Avalonia.Controls.UnitTests
private IControlTemplate CreateTemplate()
{
- return new FuncControlTemplate((control, scope) =>
+ return new FuncControlTemplate((control, scope) =>
{
var textBox =
new TextBox
diff --git a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs
index e0f46d9fa9..e01c752658 100644
--- a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs
@@ -127,6 +127,88 @@ namespace Avalonia.Controls.UnitTests
Assert.Empty(selected);
}
+ [Fact]
+ public void Intersect_Should_Remove_Items_From_Beginning()
+ {
+ var ranges = new List { new IndexRange(0, 10) };
+ var removed = new List();
+ var result = IndexRange.Intersect(ranges, new IndexRange(2, 12), removed);
+
+ Assert.Equal(2, result);
+ Assert.Equal(new[] { new IndexRange(2, 10) }, ranges);
+ Assert.Equal(new[] { new IndexRange(0, 1) }, removed);
+ }
+
+ [Fact]
+ public void Intersect_Should_Remove_Items_From_End()
+ {
+ var ranges = new List { new IndexRange(0, 10) };
+ var removed = new List();
+ var result = IndexRange.Intersect(ranges, new IndexRange(0, 8), removed);
+
+ Assert.Equal(2, result);
+ Assert.Equal(new[] { new IndexRange(0, 8) }, ranges);
+ Assert.Equal(new[] { new IndexRange(9, 10) }, removed);
+ }
+
+ [Fact]
+ public void Intersect_Should_Remove_Entire_Range_Start()
+ {
+ var ranges = new List { new IndexRange(0, 5), new IndexRange(6, 10) };
+ var removed = new List();
+ var result = IndexRange.Intersect(ranges, new IndexRange(6, 10), removed);
+
+ Assert.Equal(6, result);
+ Assert.Equal(new[] { new IndexRange(6, 10) }, ranges);
+ Assert.Equal(new[] { new IndexRange(0, 5) }, removed);
+ }
+
+ [Fact]
+ public void Intersect_Should_Remove_Entire_Range_End()
+ {
+ var ranges = new List { new IndexRange(0, 5), new IndexRange(6, 10) };
+ var removed = new List();
+ var result = IndexRange.Intersect(ranges, new IndexRange(0, 4), removed);
+
+ Assert.Equal(6, result);
+ Assert.Equal(new[] { new IndexRange(0, 4) }, ranges);
+ Assert.Equal(new[] { new IndexRange(5, 10) }, removed);
+ }
+
+ [Fact]
+ public void Intersect_Should_Remove_Entire_Range_Start_End()
+ {
+ var ranges = new List
+ {
+ new IndexRange(0, 2),
+ new IndexRange(3, 7),
+ new IndexRange(8, 10)
+ };
+ var removed = new List();
+ var result = IndexRange.Intersect(ranges, new IndexRange(3, 7), removed);
+
+ Assert.Equal(6, result);
+ Assert.Equal(new[] { new IndexRange(3, 7) }, ranges);
+ Assert.Equal(new[] { new IndexRange(0, 2), new IndexRange(8, 10) }, removed);
+ }
+
+ [Fact]
+ public void Intersect_Should_Remove_Entire_And_Partial_Range_Start_End()
+ {
+ var ranges = new List
+ {
+ new IndexRange(0, 2),
+ new IndexRange(3, 7),
+ new IndexRange(8, 10)
+ };
+ var removed = new List();
+ var result = IndexRange.Intersect(ranges, new IndexRange(4, 6), removed);
+
+ Assert.Equal(8, result);
+ Assert.Equal(new[] { new IndexRange(4, 6) }, ranges);
+ Assert.Equal(new[] { new IndexRange(0, 3), new IndexRange(7, 10) }, removed);
+ }
+
[Fact]
public void Remove_Should_Remove_Entire_Range()
{
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
index 08b9c75dbc..f27ff3928c 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
@@ -181,18 +182,21 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Fact]
- public void Child_Should_Be_Measured_With_Infinity()
+ public void Child_Should_Be_Measured_With_MaxAutoSizeHint()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new ChildControl();
var window = new Window();
- var target = CreateTarget(window);
+ var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
+ popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1200, 1000));
+ var target = CreateTarget(window, popupImpl.Object);
target.Content = child;
target.Show();
- Assert.Equal(Size.Infinity, child.MeasureSize);
+ Assert.Equal(1, child.MeasureSizes.Count);
+ Assert.Equal(new Size(1200, 1000), child.MeasureSizes[0]);
}
}
@@ -210,7 +214,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Content = child;
target.Show();
- Assert.Equal(new Size(500, 600), child.MeasureSize);
+ Assert.Equal(1, child.MeasureSizes.Count);
+ Assert.Equal(new Size(500, 600), child.MeasureSizes[0]);
}
}
@@ -228,7 +233,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Content = child;
target.Show();
- Assert.Equal(new Size(500, 600), child.MeasureSize);
+ Assert.Equal(1, child.MeasureSizes.Count);
+ Assert.Equal(new Size(500, 600), child.MeasureSizes[0]);
}
}
@@ -365,11 +371,11 @@ namespace Avalonia.Controls.UnitTests.Primitives
private class ChildControl : Control
{
- public Size MeasureSize { get; private set; }
+ public List MeasureSizes { get; } = new List();
protected override Size MeasureOverride(Size availableSize)
{
- MeasureSize = availableSize;
+ MeasureSizes.Add(availableSize);
return base.MeasureOverride(availableSize);
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs
index ebf9c40012..24e82a69d0 100644
--- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs
@@ -1512,6 +1512,47 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, raised);
}
+ [Fact]
+ public void Batch_Update_Selection_Is_Correct_Throughout()
+ {
+ var data = new[] { "foo", "bar", "baz", "qux" };
+ var target = new SelectionModel { Source = data };
+ var raised = 0;
+
+ using (target.Update())
+ {
+ target.Select(1);
+
+ Assert.Equal(new IndexPath(1), target.SelectedIndex);
+ Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices);
+ Assert.Equal("bar", target.SelectedItem);
+ Assert.Equal(new[] { "bar" }, target.SelectedItems);
+
+ target.Deselect(1);
+
+ Assert.Equal(new IndexPath(), target.SelectedIndex);
+ Assert.Empty(target.SelectedIndices);
+ Assert.Null(target.SelectedItem);
+ Assert.Empty(target.SelectedItems);
+
+ target.SelectRange(new IndexPath(1), new IndexPath(1));
+
+ Assert.Equal(new IndexPath(1), target.SelectedIndex);
+ Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices);
+ Assert.Equal("bar", target.SelectedItem);
+ Assert.Equal(new[] { "bar" }, target.SelectedItems);
+
+ target.ClearSelection();
+
+ Assert.Equal(new IndexPath(), target.SelectedIndex);
+ Assert.Empty(target.SelectedIndices);
+ Assert.Null(target.SelectedItem);
+ Assert.Empty(target.SelectedItems);
+ }
+
+ Assert.Equal(0, raised);
+ }
+
[Fact]
public void AutoSelect_Selects_When_Enabled()
{
@@ -1713,6 +1754,30 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, raised);
}
+ [Fact]
+ public void AutoSelect_Is_Applied_At_End_Of_Batch_Update()
+ {
+ var data = new[] { "foo", "bar", "baz" };
+ var target = new SelectionModel { AutoSelect = true, Source = data };
+
+ using (target.Update())
+ {
+ target.ClearSelection();
+
+ Assert.Equal(new IndexPath(), target.SelectedIndex);
+ Assert.Empty(target.SelectedIndices);
+ Assert.Null(target.SelectedItem);
+ Assert.Empty(target.SelectedItems);
+ }
+
+ Assert.Equal(new IndexPath(0), target.SelectedIndex);
+ Assert.Equal(new[] { new IndexPath(0) }, target.SelectedIndices);
+ Assert.Equal("foo", target.SelectedItem);
+ Assert.Equal(new[] { "foo" }, target.SelectedItems);
+
+ Assert.Equal(new IndexPath(0), target.SelectedIndex);
+ }
+
[Fact]
public void Can_Replace_Parent_Children_Collection()
{
@@ -1806,6 +1871,87 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, node.PropertyChangedSubscriptions);
}
+ [Fact]
+ public void Setting_SelectedIndex_To_Minus_1_Clears_Selection()
+ {
+ var data = new[] { "foo", "bar", "baz" };
+ var target = new SelectionModel { Source = data };
+ target.SelectedIndex = new IndexPath(1);
+ target.SelectedIndex = new IndexPath(-1);
+ Assert.Empty(target.SelectedIndices);
+ }
+
+ [Fact]
+ public void Assigning_Source_With_Less_Items_Than_Previous_Clears_Selection()
+ {
+ var data = new[] { "foo", "bar", "baz", "boo", "hoo" };
+ var smallerData = new[] { "foo", "bar", "baz" };
+ var target = new SelectionModel { RetainSelectionOnReset = true };
+ target.Source = data;
+ target.SelectedIndex = new IndexPath(4);
+ target.Source = smallerData;
+ Assert.Empty(target.SelectedIndices);
+ }
+
+ [Fact]
+ public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection()
+ {
+ var data = new[] { "foo", "bar", "baz" };
+ var target = new SelectionModel();
+ target.SelectedIndex = new IndexPath(4);
+ target.Source = data;
+ Assert.Empty(target.SelectedIndices);
+ }
+
+ [Fact]
+ public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection_RetainSelection()
+ {
+ var data = new[] { "foo", "bar", "baz" };
+ var target = new SelectionModel { RetainSelectionOnReset = true };
+ target.SelectedIndex = new IndexPath(4);
+ target.Source = data;
+ Assert.Empty(target.SelectedIndices);
+ }
+
+ [Fact]
+ public void Initializing_Source_With_Less_Items_Than_Multiple_Selection_Trims_Selection()
+ {
+ var data = new[] { "foo", "bar", "baz" };
+ var target = new SelectionModel { RetainSelectionOnReset = true };
+ target.Select(4);
+ target.Select(2);
+ target.Source = data;
+ Assert.Equal(1, target.SelectedIndices.Count);
+ Assert.Equal(new IndexPath(2), target.SelectedIndices.First());
+ }
+
+ [Fact]
+ public void Initializing_Source_With_Less_Items_Than_Selection_Raises_SelectionChanged()
+ {
+ var data = new[] { "foo", "bar", "baz" };
+ var target = new SelectionModel();
+ var raised = 0;
+
+ target.SelectedIndex = new IndexPath(4);
+
+ target.SelectionChanged += (s, e) =>
+ {
+ if (raised == 0)
+ {
+ Assert.Equal(new[] { Path(4) }, e.DeselectedIndices);
+ Assert.Equal(new object[] { null }, e.DeselectedItems);
+ Assert.Empty(e.SelectedIndices);
+ Assert.Empty(e.SelectedItems);
+ }
+
+ ++raised;
+ };
+
+ target.Source = data;
+
+ Assert.Equal(2, raised);
+ }
+
private int GetSubscriberCount(AvaloniaList list)
{
return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0;
diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs
index 24aacd4000..db9211ac3c 100644
--- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs
@@ -1,10 +1,12 @@
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
+using Avalonia.Controls.Utils;
using Avalonia.LogicalTree;
using Avalonia.Styling;
using Avalonia.UnitTests;
@@ -325,6 +327,28 @@ namespace Avalonia.Controls.UnitTests
Assert.NotEqual(dataContext, tabItem.Content);
}
+ [Fact]
+ public void Can_Have_Empty_Tab_Control()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+ ";
+ var loader = new Markup.Xaml.AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var tabControl = window.FindControl("tabs");
+
+ tabControl.DataContext = new { Tabs = new List() };
+ window.ApplyTemplate();
+
+ Assert.Equal(0, tabControl.Items.Count());
+ }
+ }
+
private IControlTemplate TabControlTemplate()
{
return new FuncControlTemplate((parent, scope) =>
diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
index dc5b574db7..34b37e7635 100644
--- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
@@ -102,5 +102,76 @@ namespace Avalonia.Controls.UnitTests
Assert.True(ToolTip.GetIsOpen(target));
}
}
+
+ [Fact]
+ public void Open_Class_Should_Not_Initially_Be_Added()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var toolTip = new ToolTip();
+ var window = new Window();
+
+ var decorator = new Decorator()
+ {
+ [ToolTip.TipProperty] = toolTip
+ };
+
+ window.Content = decorator;
+
+ window.ApplyTemplate();
+ window.Presenter.ApplyTemplate();
+
+ Assert.Empty(toolTip.Classes);
+ }
+ }
+
+ [Fact]
+ public void Setting_IsOpen_Should_Add_Open_Class()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var toolTip = new ToolTip();
+ var window = new Window();
+
+ var decorator = new Decorator()
+ {
+ [ToolTip.TipProperty] = toolTip
+ };
+
+ window.Content = decorator;
+
+ window.ApplyTemplate();
+ window.Presenter.ApplyTemplate();
+
+ ToolTip.SetIsOpen(decorator, true);
+
+ Assert.Equal(new[] { ":open" }, toolTip.Classes);
+ }
+ }
+
+ [Fact]
+ public void Clearing_IsOpen_Should_Remove_Open_Class()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var toolTip = new ToolTip();
+ var window = new Window();
+
+ var decorator = new Decorator()
+ {
+ [ToolTip.TipProperty] = toolTip
+ };
+
+ window.Content = decorator;
+
+ window.ApplyTemplate();
+ window.Presenter.ApplyTemplate();
+
+ ToolTip.SetIsOpen(decorator, true);
+ ToolTip.SetIsOpen(decorator, false);
+
+ Assert.Empty(toolTip.Classes);
+ }
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
index 373f3e6861..c1bd45bcad 100644
--- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
@@ -118,313 +118,340 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Clicking_Item_Should_Select_It()
{
- var tree = CreateTestTreeData();
- var target = new TreeView
+ using (Application())
{
- Template = CreateTreeViewTemplate(),
- Items = tree,
- };
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree,
+ };
- var visualRoot = new TestRoot();
- visualRoot.Child = target;
+ var visualRoot = new TestRoot();
+ visualRoot.Child = target;
- CreateNodeDataTemplate(target);
- ApplyTemplates(target);
- ExpandAll(target);
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+ ExpandAll(target);
- var item = tree[0].Children[1].Children[0];
- var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+ var item = tree[0].Children[1].Children[0];
+ var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
- Assert.NotNull(container);
+ Assert.NotNull(container);
- _mouse.Click(container);
+ _mouse.Click(container);
- Assert.Equal(item, target.SelectedItem);
- Assert.True(container.IsSelected);
+ Assert.Equal(item, target.SelectedItem);
+ Assert.True(container.IsSelected);
+ }
}
[Fact]
public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_It()
{
- var tree = CreateTestTreeData();
- var target = new TreeView
+ using (Application())
{
- Template = CreateTreeViewTemplate(),
- Items = tree
- };
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree
+ };
- var visualRoot = new TestRoot();
- visualRoot.Child = target;
+ var visualRoot = new TestRoot();
+ visualRoot.Child = target;
- CreateNodeDataTemplate(target);
- ApplyTemplates(target);
- ExpandAll(target);
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+ ExpandAll(target);
- var item = tree[0].Children[1].Children[0];
- var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+ var item = tree[0].Children[1].Children[0];
+ var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
- Assert.NotNull(container);
+ Assert.NotNull(container);
- target.SelectedItem = item;
+ target.SelectedItem = item;
- Assert.True(container.IsSelected);
+ Assert.True(container.IsSelected);
- _mouse.Click(container, modifiers: KeyModifiers.Control);
+ _mouse.Click(container, modifiers: KeyModifiers.Control);
- Assert.Null(target.SelectedItem);
- Assert.False(container.IsSelected);
+ Assert.Null(target.SelectedItem);
+ Assert.False(container.IsSelected);
+ }
}
[Fact]
public void Clicking_WithControlModifier_Not_Selected_Item_Should_Select_It()
{
- var tree = CreateTestTreeData();
- var target = new TreeView
+ using (Application())
{
- Template = CreateTreeViewTemplate(),
- Items = tree
- };
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree
+ };
- var visualRoot = new TestRoot();
- visualRoot.Child = target;
+ var visualRoot = new TestRoot();
+ visualRoot.Child = target;
- CreateNodeDataTemplate(target);
- ApplyTemplates(target);
- ExpandAll(target);
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+ ExpandAll(target);
- var item1 = tree[0].Children[1].Children[0];
- var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1);
+ var item1 = tree[0].Children[1].Children[0];
+ var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1);
- var item2 = tree[0].Children[1];
- var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2);
+ var item2 = tree[0].Children[1];
+ var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2);
- Assert.NotNull(container1);
- Assert.NotNull(container2);
+ Assert.NotNull(container1);
+ Assert.NotNull(container2);
- target.SelectedItem = item1;
+ target.SelectedItem = item1;
- Assert.True(container1.IsSelected);
+ Assert.True(container1.IsSelected);
- _mouse.Click(container2, modifiers: KeyModifiers.Control);
-
- Assert.Equal(item2, target.SelectedItem);
- Assert.False(container1.IsSelected);
- Assert.True(container2.IsSelected);
+ _mouse.Click(container2, modifiers: KeyModifiers.Control);
+
+ Assert.Equal(item2, target.SelectedItem);
+ Assert.False(container1.IsSelected);
+ Assert.True(container2.IsSelected);
+ }
}
[Fact]
public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_And_Remove_From_SelectedItems()
{
- var tree = CreateTestTreeData();
- var target = new TreeView
+ using (Application())
{
- Template = CreateTreeViewTemplate(),
- Items = tree,
- SelectionMode = SelectionMode.Multiple
- };
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree,
+ SelectionMode = SelectionMode.Multiple
+ };
- var visualRoot = new TestRoot();
- visualRoot.Child = target;
+ var visualRoot = new TestRoot();
+ visualRoot.Child = target;
- CreateNodeDataTemplate(target);
- ApplyTemplates(target);
- ExpandAll(target);
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+ ExpandAll(target);
- var rootNode = tree[0];
+ var rootNode = tree[0];
- var item1 = rootNode.Children[0];
- var item2 = rootNode.Children.Last();
+ var item1 = rootNode.Children[0];
+ var item2 = rootNode.Children.Last();
- var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1);
- var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2);
+ var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1);
+ var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2);
- ClickContainer(item1Container, KeyModifiers.Control);
- Assert.True(item1Container.IsSelected);
+ ClickContainer(item1Container, KeyModifiers.Control);
+ Assert.True(item1Container.IsSelected);
- ClickContainer(item2Container, KeyModifiers.Control);
- Assert.True(item2Container.IsSelected);
+ ClickContainer(item2Container, KeyModifiers.Control);
+ Assert.True(item2Container.IsSelected);
- Assert.Equal(new[] {item1, item2}, target.Selection.SelectedItems.OfType());
+ Assert.Equal(new[] { item1, item2 }, target.Selection.SelectedItems.OfType());
- ClickContainer(item1Container, KeyModifiers.Control);
- Assert.False(item1Container.IsSelected);
+ ClickContainer(item1Container, KeyModifiers.Control);
+ Assert.False(item1Container.IsSelected);
- Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType());
+ Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType());
+ }
}
[Fact]
public void Clicking_WithShiftModifier_DownDirection_Should_Select_Range_Of_Items()
{
- var tree = CreateTestTreeData();
- var target = new TreeView
+ using (Application())
{
- Template = CreateTreeViewTemplate(),
- Items = tree,
- SelectionMode = SelectionMode.Multiple
- };
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree,
+ SelectionMode = SelectionMode.Multiple
+ };
- var visualRoot = new TestRoot();
- visualRoot.Child = target;
+ var visualRoot = new TestRoot();
+ visualRoot.Child = target;
- CreateNodeDataTemplate(target);
- ApplyTemplates(target);
- ExpandAll(target);
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+ ExpandAll(target);
- var rootNode = tree[0];
+ var rootNode = tree[0];
- var from = rootNode.Children[0];
- var to = rootNode.Children.Last();
+ var from = rootNode.Children[0];
+ var to = rootNode.Children.Last();
- var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
- var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
+ var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
+ var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
- ClickContainer(fromContainer, KeyModifiers.None);
+ ClickContainer(fromContainer, KeyModifiers.None);
- Assert.True(fromContainer.IsSelected);
+ Assert.True(fromContainer.IsSelected);
- ClickContainer(toContainer, KeyModifiers.Shift);
- AssertChildrenSelected(target, rootNode);
+ ClickContainer(toContainer, KeyModifiers.Shift);
+ AssertChildrenSelected(target, rootNode);
+ }
}
[Fact]
public void Clicking_WithShiftModifier_UpDirection_Should_Select_Range_Of_Items()
{
- var tree = CreateTestTreeData();
- var target = new TreeView
+ using (Application())
{
- Template = CreateTreeViewTemplate(),
- Items = tree,
- SelectionMode = SelectionMode.Multiple
- };
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree,
+ SelectionMode = SelectionMode.Multiple
+ };
- var visualRoot = new TestRoot();
- visualRoot.Child = target;
+ var visualRoot = new TestRoot();
+ visualRoot.Child = target;
- CreateNodeDataTemplate(target);
- ApplyTemplates(target);
- ExpandAll(target);
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+ ExpandAll(target);
- var rootNode = tree[0];
+ var rootNode = tree[0];
- var from = rootNode.Children.Last();
- var to = rootNode.Children[0];
+ var from = rootNode.Children.Last();
+ var to = rootNode.Children[0];
- var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
- var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
+ var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
+ var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
- ClickContainer(fromContainer, KeyModifiers.None);
+ ClickContainer(fromContainer, KeyModifiers.None);
- Assert.True(fromContainer.IsSelected);
+ Assert.True(fromContainer.IsSelected);
- ClickContainer(toContainer, KeyModifiers.Shift);
- AssertChildrenSelected(target, rootNode);
+ ClickContainer(toContainer, KeyModifiers.Shift);
+ AssertChildrenSelected(target, rootNode);
+ }
}
[Fact]
public void Clicking_First_Item_Of_SelectedItems_Should_Select_Only_It()
{
- var tree = CreateTestTreeData();
- var target = new TreeView
+ using (Application())
{
- Template = CreateTreeViewTemplate(),
- Items = tree,
- SelectionMode = SelectionMode.Multiple
- };
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree,
+ SelectionMode = SelectionMode.Multiple
+ };
- var visualRoot = new TestRoot();
- visualRoot.Child = target;
+ var visualRoot = new TestRoot();
+ visualRoot.Child = target;
- CreateNodeDataTemplate(target);
- ApplyTemplates(target);
- ExpandAll(target);
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+ ExpandAll(target);
- var rootNode = tree[0];
+ var rootNode = tree[0];
- var from = rootNode.Children.Last();
- var to = rootNode.Children[0];
+ var from = rootNode.Children.Last();
+ var to = rootNode.Children[0];
- var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
- var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
+ var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
+ var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
- ClickContainer(fromContainer, KeyModifiers.None);
+ ClickContainer(fromContainer, KeyModifiers.None);
- ClickContainer(toContainer, KeyModifiers.Shift);
- AssertChildrenSelected(target, rootNode);
+ ClickContainer(toContainer, KeyModifiers.Shift);
+ AssertChildrenSelected(target, rootNode);
- ClickContainer(fromContainer, KeyModifiers.None);
+ ClickContainer(fromContainer, KeyModifiers.None);
- Assert.True(fromContainer.IsSelected);
+ Assert.True(fromContainer.IsSelected);
- foreach (var child in rootNode.Children)
- {
- if (child == from)
+ foreach (var child in rootNode.Children)
{
- continue;
- }
+ if (child == from)
+ {
+ continue;
+ }
- var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child);
+ var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child);
- Assert.False(container.IsSelected);
+ Assert.False(container.IsSelected);
+ }
}
}
[Fact]
public void Setting_SelectedItem_Should_Set_Container_Selected()
{
- var tree = CreateTestTreeData();
- var target = new TreeView
+ using (Application())
{
- Template = CreateTreeViewTemplate(),
- Items = tree,
- };
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree,
+ };
- var visualRoot = new TestRoot();
- visualRoot.Child = target;
+ var visualRoot = new TestRoot();
+ visualRoot.Child = target;
- CreateNodeDataTemplate(target);
- ApplyTemplates(target);
- ExpandAll(target);
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+ ExpandAll(target);
- var item = tree[0].Children[1].Children[0];
- var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+ var item = tree[0].Children[1].Children[0];
+ var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
- Assert.NotNull(container);
+ Assert.NotNull(container);
- target.SelectedItem = item;
+ target.SelectedItem = item;
- Assert.True(container.IsSelected);
+ Assert.True(container.IsSelected);
+ }
}
[Fact]
public void Setting_SelectedItem_Should_Raise_SelectedItemChanged_Event()
{
- var tree = CreateTestTreeData();
- var target = new TreeView
+ using (Application())
{
- Template = CreateTreeViewTemplate(),
- Items = tree,
- };
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree,
+ };
- var visualRoot = new TestRoot();
- visualRoot.Child = target;
+ var visualRoot = new TestRoot();
+ visualRoot.Child = target;
- CreateNodeDataTemplate(target);
- ApplyTemplates(target);
- ExpandAll(target);
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+ ExpandAll(target);
- var item = tree[0].Children[1].Children[0];
+ var item = tree[0].Children[1].Children[0];
- var called = false;
- target.SelectionChanged += (s, e) =>
- {
- Assert.Empty(e.RemovedItems);
- Assert.Equal(1, e.AddedItems.Count);
- Assert.Same(item, e.AddedItems[0]);
- called = true;
- };
+ var called = false;
+ target.SelectionChanged += (s, e) =>
+ {
+ Assert.Empty(e.RemovedItems);
+ Assert.Equal(1, e.AddedItems.Count);
+ Assert.Same(item, e.AddedItems[0]);
+ called = true;
+ };
- target.SelectedItem = item;
- Assert.True(called);
+ target.SelectedItem = item;
+ Assert.True(called);
+ }
}
[Fact]
@@ -564,7 +591,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node()
{
- using (UnitTestApplication.Start(TestServices.RealFocus))
+ using (Application())
{
var focus = FocusManager.Instance;
var navigation = AvaloniaLocator.Current.GetService();
@@ -647,7 +674,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Pressing_SelectAll_Gesture_With_Downward_Range_Selected_Should_Select_All_Nodes()
{
- using (UnitTestApplication.Start())
+ using (Application())
{
var tree = CreateTestTreeData();
var target = new TreeView
@@ -694,7 +721,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Pressing_SelectAll_Gesture_With_Upward_Range_Selected_Should_Select_All_Nodes()
{
- using (UnitTestApplication.Start())
+ using (Application())
{
var tree = CreateTestTreeData();
var target = new TreeView
@@ -768,97 +795,106 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection()
{
- var tree = CreateTestTreeData();
- var target = new TreeView
+ using (Application())
{
- Template = CreateTreeViewTemplate(),
- Items = tree,
- SelectionMode = SelectionMode.Multiple,
- };
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree,
+ SelectionMode = SelectionMode.Multiple,
+ };
- var visualRoot = new TestRoot();
- visualRoot.Child = target;
+ var visualRoot = new TestRoot();
+ visualRoot.Child = target;
- CreateNodeDataTemplate(target);
- ApplyTemplates(target);
- target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]);
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+ target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]);
- var rootNode = tree[0];
- var to = rootNode.Children[0];
- var then = rootNode.Children[1];
+ var rootNode = tree[0];
+ var to = rootNode.Children[0];
+ var then = rootNode.Children[1];
- var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode);
- var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
- var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then);
+ var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode);
+ var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
+ var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then);
- ClickContainer(fromContainer, KeyModifiers.None);
- ClickContainer(toContainer, KeyModifiers.Shift);
+ ClickContainer(fromContainer, KeyModifiers.None);
+ ClickContainer(toContainer, KeyModifiers.Shift);
- Assert.Equal(2, target.Selection.SelectedItems.Count);
+ Assert.Equal(2, target.Selection.SelectedItems.Count);
- _mouse.Click(thenContainer, MouseButton.Right);
+ _mouse.Click(thenContainer, MouseButton.Right);
- Assert.Equal(1, target.Selection.SelectedItems.Count);
+ Assert.Equal(1, target.Selection.SelectedItems.Count);
+ }
}
[Fact]
public void Shift_Right_Click_Should_Not_Select_Multiple()
{
- var tree = CreateTestTreeData();
- var target = new TreeView
+ using (Application())
{
- Template = CreateTreeViewTemplate(),
- Items = tree,
- SelectionMode = SelectionMode.Multiple,
- };
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree,
+ SelectionMode = SelectionMode.Multiple,
+ };
- var visualRoot = new TestRoot();
- visualRoot.Child = target;
+ var visualRoot = new TestRoot();
+ visualRoot.Child = target;
- CreateNodeDataTemplate(target);
- ApplyTemplates(target);
- target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]);
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+ target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]);
- var rootNode = tree[0];
- var from = rootNode.Children[0];
- var to = rootNode.Children[1];
- var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
- var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
+ var rootNode = tree[0];
+ var from = rootNode.Children[0];
+ var to = rootNode.Children[1];
+ var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
+ var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
- _mouse.Click(fromContainer);
- _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift);
+ _mouse.Click(fromContainer);
+ _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift);
- Assert.Equal(1, target.Selection.SelectedItems.Count);
+ Assert.Equal(1, target.Selection.SelectedItems.Count);
+ }
}
[Fact]
public void Ctrl_Right_Click_Should_Not_Select_Multiple()
{
- var tree = CreateTestTreeData();
- var target = new TreeView
+ using (Application())
{
- Template = CreateTreeViewTemplate(),
- Items = tree,
- SelectionMode = SelectionMode.Multiple,
- };
+ var tree = CreateTestTreeData();
+ var target = new TreeView
+ {
+ Template = CreateTreeViewTemplate(),
+ Items = tree,
+ SelectionMode = SelectionMode.Multiple,
+ };
- var visualRoot = new TestRoot();
- visualRoot.Child = target;
+ var visualRoot = new TestRoot();
+ visualRoot.Child = target;
- CreateNodeDataTemplate(target);
- ApplyTemplates(target);
- target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]);
+ CreateNodeDataTemplate(target);
+ ApplyTemplates(target);
+ target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]);
- var rootNode = tree[0];
- var from = rootNode.Children[0];
- var to = rootNode.Children[1];
- var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
- var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
+ var rootNode = tree[0];
+ var from = rootNode.Children[0];
+ var to = rootNode.Children[1];
+ var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
+ var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
- _mouse.Click(fromContainer);
- _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control);
+ _mouse.Click(fromContainer);
+ _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control);
- Assert.Equal(1, target.Selection.SelectedItems.Count);
+ Assert.Equal(1, target.Selection.SelectedItems.Count);
+ }
}
[Fact]
@@ -944,7 +980,7 @@ namespace Avalonia.Controls.UnitTests
public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection()
{
/// Issue #2980.
- using (UnitTestApplication.Start(TestServices.RealStyler))
+ using (Application())
{
var target = new DerivedTreeView
{
@@ -1183,12 +1219,12 @@ namespace Avalonia.Controls.UnitTests
}
}
- void ClickContainer(IControl container, KeyModifiers modifiers)
+ private void ClickContainer(IControl container, KeyModifiers modifiers)
{
_mouse.Click(container, modifiers: modifiers);
}
- void AssertChildrenSelected(TreeView treeView, Node rootNode)
+ private void AssertChildrenSelected(TreeView treeView, Node rootNode)
{
foreach (var child in rootNode.Children)
{
@@ -1198,6 +1234,16 @@ namespace Avalonia.Controls.UnitTests
}
}
+ private IDisposable Application()
+ {
+ return UnitTestApplication.Start(
+ TestServices.MockThreadingInterface.With(
+ focusManager: new FocusManager(),
+ keyboardDevice: () => new KeyboardDevice(),
+ keyboardNavigation: new KeyboardNavigationHandler(),
+ inputManager: new InputManager()));
+ }
+
private class Node : NotifyingBase
{
private IAvaloniaList _children;
diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
index 80c8a34ffd..e2b0def00b 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
@@ -297,12 +297,12 @@ namespace Avalonia.Controls.UnitTests
{
var parentWindowImpl = MockWindowingPlatform.CreateWindowMock();
parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480));
- parentWindowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080));
+ parentWindowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1920, 1080));
parentWindowImpl.Setup(x => x.Scaling).Returns(1);
var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200));
- windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080));
+ windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1920, 1080));
windowImpl.Setup(x => x.Scaling).Returns(1);
var parentWindowServices = TestServices.StyledWindow.With(
@@ -381,12 +381,15 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
- public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
+ public void Child_Should_Be_Measured_With_MaxAutoSizeHint_If_SizeToContent_Is_WidthAndHeight()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
+ var windowImpl = MockWindowingPlatform.CreateWindowMock();
+ windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1200, 1000));
+
var child = new ChildControl();
- var target = new Window
+ var target = new Window(windowImpl.Object)
{
Width = 100,
Height = 50,
@@ -394,10 +397,10 @@ namespace Avalonia.Controls.UnitTests
Content = child
};
- Show(target);
+ target.Show();
Assert.Equal(1, child.MeasureSizes.Count);
- Assert.Equal(Size.Infinity, child.MeasureSizes[0]);
+ Assert.Equal(new Size(1200, 1000), child.MeasureSizes[0]);
}
}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs
index 5773f5e6b0..64f5eb2a0e 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs
@@ -113,6 +113,50 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal("(null)", target.Text);
}
+ [Fact]
+ public void Should_Pass_UnsetValue_To_Converter_For_Broken_Binding()
+ {
+ var source = new { A = 1, B = 2, C = 3 };
+ var target = new TextBlock { DataContext = source };
+
+ var binding = new MultiBinding
+ {
+ Converter = new ConcatConverter(),
+ Bindings = new[]
+ {
+ new Binding { Path = "A" },
+ new Binding { Path = "B" },
+ new Binding { Path = "Missing" },
+ },
+ };
+
+ target.Bind(TextBlock.TextProperty, binding);
+
+ Assert.Equal("1,2,(unset)", target.Text);
+ }
+
+ [Fact]
+ public void Should_Pass_FallbackValue_To_Converter_For_Broken_Binding()
+ {
+ var source = new { A = 1, B = 2, C = 3 };
+ var target = new TextBlock { DataContext = source };
+
+ var binding = new MultiBinding
+ {
+ Converter = new ConcatConverter(),
+ Bindings = new[]
+ {
+ new Binding { Path = "A" },
+ new Binding { Path = "B" },
+ new Binding { Path = "Missing", FallbackValue = "Fallback" },
+ },
+ };
+
+ target.Bind(TextBlock.TextProperty, binding);
+
+ Assert.Equal("1,2,Fallback", target.Text);
+ }
+
private class ConcatConverter : IMultiValueConverter
{
public object Convert(IList values, Type targetType, object parameter, CultureInfo culture)
diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs
index 4f7264f2f2..8cf74c36f4 100644
--- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs
+++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs
@@ -22,7 +22,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
Assert.Equal(
new BindingNotification(
- new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error),
+ new MissingMemberException("Could not find a matching property accessor for 'Baz' on '1'"), BindingErrorType.Error),
result);
GC.KeepAlive(data);
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
index 43c7525939..6730e3134d 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
@@ -1,6 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
using Avalonia.Data;
+using Avalonia.Data.Converters;
using Avalonia.UnitTests;
+using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Data
@@ -54,5 +61,51 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
Assert.Equal("bar", textBlock.Text);
}
}
+
+ [Fact]
+ public void MultiBinding_TemplatedParent_Works()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ";
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var textBox = window.FindControl("textBox");
+
+ window.ApplyTemplate();
+ textBox.ApplyTemplate();
+
+ var target = (TextPresenter)textBox.GetVisualChildren().Single();
+ Assert.Equal("Foo,Bar", target.Text);
+ }
+ }
+ }
+
+ public class ConcatConverter : IMultiValueConverter
+ {
+ public static ConcatConverter Instance { get; } = new ConcatConverter();
+
+ public object Convert(IList values, Type targetType, object parameter, CultureInfo culture)
+ {
+ return string.Join(",", values);
+ }
}
}
diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
index b3e4b4edbc..ee45433089 100644
--- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
+++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
@@ -28,6 +28,7 @@ namespace Avalonia.UnitTests
windowImpl.SetupAllProperties();
windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
+ windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize);
windowImpl.Setup(x => x.Scaling).Returns(1);
windowImpl.Setup(x => x.Screen).Returns(CreateScreenMock().Object);
windowImpl.Setup(x => x.Position).Returns(() => position);
@@ -79,6 +80,7 @@ namespace Avalonia.UnitTests
popupImpl.SetupAllProperties();
popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
+ popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize);
popupImpl.Setup(x => x.Scaling).Returns(1);
popupImpl.Setup(x => x.PopupPositioner).Returns(positioner);
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs
index e17fd47ff8..f3f3c9a4ca 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs
@@ -17,6 +17,41 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0xff, result.A);
}
+ [Fact]
+ public void Try_Parse_Parses_RGB_Hash_Color()
+ {
+ var success = Color.TryParse("#ff8844", out Color result);
+
+ Assert.True(success);
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x88, result.G);
+ Assert.Equal(0x44, result.B);
+ Assert.Equal(0xff, result.A);
+ }
+
+ [Fact]
+ public void Parse_Parses_RGB_Hash_Shorthand_Color()
+ {
+ var result = Color.Parse("#f84");
+
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x88, result.G);
+ Assert.Equal(0x44, result.B);
+ Assert.Equal(0xff, result.A);
+ }
+
+ [Fact]
+ public void Try_Parse_Parses_RGB_Hash_Shorthand_Color()
+ {
+ var success = Color.TryParse("#f84", out Color result);
+
+ Assert.True(success);
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x88, result.G);
+ Assert.Equal(0x44, result.B);
+ Assert.Equal(0xff, result.A);
+ }
+
[Fact]
public void Parse_Parses_ARGB_Hash_Color()
{
@@ -28,6 +63,41 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0x40, result.A);
}
+ [Fact]
+ public void Try_Parse_Parses_ARGB_Hash_Color()
+ {
+ var success = Color.TryParse("#40ff8844", out Color result);
+
+ Assert.True(success);
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x88, result.G);
+ Assert.Equal(0x44, result.B);
+ Assert.Equal(0x40, result.A);
+ }
+
+ [Fact]
+ public void Parse_Parses_ARGB_Hash_Shorthand_Color()
+ {
+ var result = Color.Parse("#4f84");
+
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x88, result.G);
+ Assert.Equal(0x44, result.B);
+ Assert.Equal(0x44, result.A);
+ }
+
+ [Fact]
+ public void Try_Parse_Parses_ARGB_Hash_Shorthand_Color()
+ {
+ var success = Color.TryParse("#4f84", out Color result);
+
+ Assert.True(success);
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x88, result.G);
+ Assert.Equal(0x44, result.B);
+ Assert.Equal(0x44, result.A);
+ }
+
[Fact]
public void Parse_Parses_Named_Color_Lowercase()
{
@@ -39,6 +109,18 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0xff, result.A);
}
+ [Fact]
+ public void TryParse_Parses_Named_Color_Lowercase()
+ {
+ var success = Color.TryParse("red", out Color result);
+
+ Assert.True(success);
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x00, result.G);
+ Assert.Equal(0x00, result.B);
+ Assert.Equal(0xff, result.A);
+ }
+
[Fact]
public void Parse_Parses_Named_Color_Uppercase()
{
@@ -50,22 +132,52 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0xff, result.A);
}
+ [Fact]
+ public void TryParse_Parses_Named_Color_Uppercase()
+ {
+ var success = Color.TryParse("RED", out Color result);
+
+ Assert.True(success);
+ Assert.Equal(0xff, result.R);
+ Assert.Equal(0x00, result.G);
+ Assert.Equal(0x00, result.B);
+ Assert.Equal(0xff, result.A);
+ }
+
[Fact]
public void Parse_Hex_Value_Doesnt_Accept_Too_Few_Chars()
{
Assert.Throws(() => Color.Parse("#ff"));
}
+ [Fact]
+ public void TryParse_Hex_Value_Doesnt_Accept_Too_Few_Chars()
+ {
+ Assert.False(Color.TryParse("#ff", out _));
+ }
+
[Fact]
public void Parse_Hex_Value_Doesnt_Accept_Too_Many_Chars()
{
Assert.Throws(() => Color.Parse("#ff5555555"));
}
+ [Fact]
+ public void TryParse_Hex_Value_Doesnt_Accept_Too_Many_Chars()
+ {
+ Assert.False(Color.TryParse("#ff5555555", out _));
+ }
+
[Fact]
public void Parse_Hex_Value_Doesnt_Accept_Invalid_Number()
{
Assert.Throws(() => Color.Parse("#ff808g80"));
}
+
+ [Fact]
+ public void TryParse_Hex_Value_Doesnt_Accept_Invalid_Number()
+ {
+ Assert.False(Color.TryParse("#ff808g80", out _));
+ }
}
}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs
index ff1d17164e..6ef48b6161 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs
@@ -1,4 +1,5 @@
-using System.Globalization;
+using System;
+using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
@@ -6,11 +7,93 @@ namespace Avalonia.Visuals.UnitTests.Media
public class MatrixTests
{
[Fact]
- public void Parse_Parses()
+ public void Can_Parse()
{
var matrix = Matrix.Parse("1,2,3,-4,5 6");
var expected = new Matrix(1, 2, 3, -4, 5, 6);
Assert.Equal(expected, matrix);
}
+
+ [Fact]
+ public void Singular_Has_No_Inverse()
+ {
+ var matrix = new Matrix(0, 0, 0, 0, 0, 0);
+
+ Assert.False(matrix.HasInverse);
+ }
+
+ [Fact]
+ public void Identity_Has_Inverse()
+ {
+ var matrix = Matrix.Identity;
+
+ Assert.True(matrix.HasInverse);
+ }
+
+ [Fact]
+ public void Can_Decompose_Translation()
+ {
+ var matrix = Matrix.CreateTranslation(5, 10);
+
+ var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed);
+
+ Assert.Equal(true, result);
+ Assert.Equal(5, decomposed.Translate.X);
+ Assert.Equal(10, decomposed.Translate.Y);
+ }
+
+ [Theory]
+ [InlineData(30d)]
+ [InlineData(0d)]
+ [InlineData(90d)]
+ [InlineData(270d)]
+ public void Can_Decompose_Angle(double angleDeg)
+ {
+ var angleRad = MathUtilities.Deg2Rad(angleDeg);
+
+ var matrix = Matrix.CreateRotation(angleRad);
+
+ var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed);
+
+ Assert.Equal(true, result);
+
+ var expected = NormalizeAngle(angleRad);
+ var actual = NormalizeAngle(decomposed.Angle);
+
+ Assert.Equal(expected, actual, 4);
+ }
+
+ [Theory]
+ [InlineData(1d, 1d)]
+ [InlineData(-1d, 1d)]
+ [InlineData(1d, -1d)]
+ [InlineData(5d, 10d)]
+ public void Can_Decompose_Scale(double x, double y)
+ {
+ var matrix = Matrix.CreateScale(x, y);
+
+ var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed);
+
+ Assert.Equal(true, result);
+ Assert.Equal(x, decomposed.Scale.X);
+ Assert.Equal(y, decomposed.Scale.Y);
+ }
+
+ private static double NormalizeAngle(double rad)
+ {
+ double twoPi = 2 * Math.PI;
+
+ while (rad < 0)
+ {
+ rad += twoPi;
+ }
+
+ while (rad > twoPi)
+ {
+ rad -= twoPi;
+ }
+
+ return rad;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs
new file mode 100644
index 0000000000..856b4615a5
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs
@@ -0,0 +1,229 @@
+using Avalonia.Media.Transformation;
+using Avalonia.Utilities;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+ public class TransformOperationsTests
+ {
+ [Theory]
+ [InlineData("translate(10px)", 10d, 0d)]
+ [InlineData("translate(10px, 10px)", 10d, 10d)]
+ [InlineData("translate(0px, 10px)", 0d, 10d)]
+ [InlineData("translate(10px, 0px)", 10d, 0d)]
+ [InlineData("translateX(10px)", 10d, 0d)]
+ [InlineData("translateY(10px)", 0d, 10d)]
+ public void Can_Parse_Translation(string data, double x, double y)
+ {
+ var transform = TransformOperations.Parse(data);
+
+ var operations = transform.Operations;
+
+ Assert.Single(operations);
+ Assert.Equal(TransformOperation.OperationType.Translate, operations[0].Type);
+ Assert.Equal(x, operations[0].Data.Translate.X);
+ Assert.Equal(y, operations[0].Data.Translate.Y);
+ }
+
+ [Theory]
+ [InlineData("rotate(90deg)", 90d)]
+ [InlineData("rotate(0.5turn)", 180d)]
+ [InlineData("rotate(200grad)", 180d)]
+ [InlineData("rotate(3.14159265rad)", 180d)]
+ public void Can_Parse_Rotation(string data, double angleDeg)
+ {
+ var transform = TransformOperations.Parse(data);
+
+ var operations = transform.Operations;
+
+ Assert.Single(operations);
+ Assert.Equal(TransformOperation.OperationType.Rotate, operations[0].Type);
+ Assert.Equal(MathUtilities.Deg2Rad(angleDeg), operations[0].Data.Rotate.Angle, 4);
+ }
+
+ [Theory]
+ [InlineData("scale(10)", 10d, 10d)]
+ [InlineData("scale(10, 10)", 10d, 10d)]
+ [InlineData("scale(0, 10)", 0d, 10d)]
+ [InlineData("scale(10, 0)", 10d, 0d)]
+ [InlineData("scaleX(10)", 10d, 1d)]
+ [InlineData("scaleY(10)", 1d, 10d)]
+ public void Can_Parse_Scale(string data, double x, double y)
+ {
+ var transform = TransformOperations.Parse(data);
+
+ var operations = transform.Operations;
+
+ Assert.Single(operations);
+ Assert.Equal(TransformOperation.OperationType.Scale, operations[0].Type);
+ Assert.Equal(x, operations[0].Data.Scale.X);
+ Assert.Equal(y, operations[0].Data.Scale.Y);
+ }
+
+ [Theory]
+ [InlineData("skew(90deg)", 90d, 0d)]
+ [InlineData("skew(0.5turn)", 180d, 0d)]
+ [InlineData("skew(200grad)", 180d, 0d)]
+ [InlineData("skew(3.14159265rad)", 180d, 0d)]
+ [InlineData("skewX(90deg)", 90d, 0d)]
+ [InlineData("skewX(0.5turn)", 180d, 0d)]
+ [InlineData("skewX(200grad)", 180d, 0d)]
+ [InlineData("skewX(3.14159265rad)", 180d, 0d)]
+ [InlineData("skew(0, 90deg)", 0d, 90d)]
+ [InlineData("skew(0, 0.5turn)", 0d, 180d)]
+ [InlineData("skew(0, 200grad)", 0d, 180d)]
+ [InlineData("skew(0, 3.14159265rad)", 0d, 180d)]
+ [InlineData("skewY(90deg)", 0d, 90d)]
+ [InlineData("skewY(0.5turn)", 0d, 180d)]
+ [InlineData("skewY(200grad)", 0d, 180d)]
+ [InlineData("skewY(3.14159265rad)", 0d, 180d)]
+ [InlineData("skew(90deg, 90deg)", 90d, 90d)]
+ [InlineData("skew(0.5turn, 0.5turn)", 180d, 180d)]
+ [InlineData("skew(200grad, 200grad)", 180d, 180d)]
+ [InlineData("skew(3.14159265rad, 3.14159265rad)", 180d, 180d)]
+ public void Can_Parse_Skew(string data, double x, double y)
+ {
+ var transform = TransformOperations.Parse(data);
+
+ var operations = transform.Operations;
+
+ Assert.Single(operations);
+ Assert.Equal(TransformOperation.OperationType.Skew, operations[0].Type);
+ Assert.Equal(MathUtilities.Deg2Rad(x), operations[0].Data.Skew.X, 4);
+ Assert.Equal(MathUtilities.Deg2Rad(y), operations[0].Data.Skew.Y, 4);
+ }
+
+ [Fact]
+ public void Can_Parse_Compound_Operations()
+ {
+ var data = "scale(1,2) translate(3px,4px) rotate(5deg) skew(6deg,7deg)";
+
+ var transform = TransformOperations.Parse(data);
+
+ var operations = transform.Operations;
+
+ Assert.Equal(TransformOperation.OperationType.Scale, operations[0].Type);
+ Assert.Equal(1, operations[0].Data.Scale.X);
+ Assert.Equal(2, operations[0].Data.Scale.Y);
+
+ Assert.Equal(TransformOperation.OperationType.Translate, operations[1].Type);
+ Assert.Equal(3, operations[1].Data.Translate.X);
+ Assert.Equal(4, operations[1].Data.Translate.Y);
+
+ Assert.Equal(TransformOperation.OperationType.Rotate, operations[2].Type);
+ Assert.Equal(MathUtilities.Deg2Rad(5), operations[2].Data.Rotate.Angle);
+
+ Assert.Equal(TransformOperation.OperationType.Skew, operations[3].Type);
+ Assert.Equal(MathUtilities.Deg2Rad(6), operations[3].Data.Skew.X);
+ Assert.Equal(MathUtilities.Deg2Rad(7), operations[3].Data.Skew.Y);
+ }
+
+ [Fact]
+ public void Can_Parse_Matrix_Operation()
+ {
+ var data = "matrix(1,2,3,4,5,6)";
+
+ var transform = TransformOperations.Parse(data);
+
+ var operations = transform.Operations;
+
+ Assert.Single(operations);
+ Assert.Equal(TransformOperation.OperationType.Matrix, operations[0].Type);
+
+ var expectedMatrix = new Matrix(1, 2, 3, 4, 5, 6);
+
+ Assert.Equal(expectedMatrix, operations[0].Matrix);
+ }
+
+ [Theory]
+ [InlineData(0d, 10d, 0d)]
+ [InlineData(0.5d, 5d, 10d)]
+ [InlineData(1d, 0d, 20d)]
+ public void Can_Interpolate_Translation(double progress, double x, double y)
+ {
+ var from = TransformOperations.Parse("translateX(10px)");
+ var to = TransformOperations.Parse("translateY(20px)");
+
+ var interpolated = TransformOperations.Interpolate(from, to, progress);
+
+ var operations = interpolated.Operations;
+
+ Assert.Single(operations);
+ Assert.Equal(TransformOperation.OperationType.Translate, operations[0].Type);
+ Assert.Equal(x, operations[0].Data.Translate.X);
+ Assert.Equal(y, operations[0].Data.Translate.Y);
+ }
+
+ [Theory]
+ [InlineData(0d, 10d, 1d)]
+ [InlineData(0.5d, 5.5d, 10.5d)]
+ [InlineData(1d, 1d, 20d)]
+ public void Can_Interpolate_Scale(double progress, double x, double y)
+ {
+ var from = TransformOperations.Parse("scaleX(10)");
+ var to = TransformOperations.Parse("scaleY(20)");
+
+ var interpolated = TransformOperations.Interpolate(from, to, progress);
+
+ var operations = interpolated.Operations;
+
+ Assert.Single(operations);
+ Assert.Equal(TransformOperation.OperationType.Scale, operations[0].Type);
+ Assert.Equal(x, operations[0].Data.Scale.X);
+ Assert.Equal(y, operations[0].Data.Scale.Y);
+ }
+
+ [Theory]
+ [InlineData(0d, 10d, 0d)]
+ [InlineData(0.5d, 5d, 10d)]
+ [InlineData(1d, 0d, 20d)]
+ public void Can_Interpolate_Skew(double progress, double x, double y)
+ {
+ var from = TransformOperations.Parse("skewX(10deg)");
+ var to = TransformOperations.Parse("skewY(20deg)");
+
+ var interpolated = TransformOperations.Interpolate(from, to, progress);
+
+ var operations = interpolated.Operations;
+
+ Assert.Single(operations);
+ Assert.Equal(TransformOperation.OperationType.Skew, operations[0].Type);
+ Assert.Equal(MathUtilities.Deg2Rad(x), operations[0].Data.Skew.X);
+ Assert.Equal(MathUtilities.Deg2Rad(y), operations[0].Data.Skew.Y);
+ }
+
+ [Theory]
+ [InlineData(0d, 10d)]
+ [InlineData(0.5d, 15d)]
+ [InlineData(1d,20d)]
+ public void Can_Interpolate_Rotation(double progress, double angle)
+ {
+ var from = TransformOperations.Parse("rotate(10deg)");
+ var to = TransformOperations.Parse("rotate(20deg)");
+
+ var interpolated = TransformOperations.Interpolate(from, to, progress);
+
+ var operations = interpolated.Operations;
+
+ Assert.Single(operations);
+ Assert.Equal(TransformOperation.OperationType.Rotate, operations[0].Type);
+ Assert.Equal(MathUtilities.Deg2Rad(angle), operations[0].Data.Rotate.Angle);
+ }
+
+ [Fact]
+ public void Interpolation_Fallback_To_Matrix()
+ {
+ double progress = 0.5d;
+
+ var from = TransformOperations.Parse("rotate(45deg)");
+ var to = TransformOperations.Parse("translate(100px, 100px) rotate(1215deg)");
+
+ var interpolated = TransformOperations.Interpolate(from, to, progress);
+
+ var operations = interpolated.Operations;
+
+ Assert.Single(operations);
+ Assert.Equal(TransformOperation.OperationType.Matrix, operations[0].Type);
+ }
+ }
+}