diff --git a/Avalonia.sln b/Avalonia.sln
index e8d5034fb0..5bff2fa0a0 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -95,8 +95,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.UnitTests", "tests
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Benchmarks", "tests\Avalonia.Benchmarks\Avalonia.Benchmarks.csproj", "{410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Logging.Serilog", "src\Avalonia.Logging.Serilog\Avalonia.Logging.Serilog.csproj", "{B61B66A3-B82D-4875-8001-89D3394FE0C9}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport", "src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj", "{799A7BB5-3C2C-48B6-85A7-406A12C420DA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog", "samples\ControlCatalog\ControlCatalog.csproj", "{D0A739B9-3C68-4BA6-A328-41606954B6BD}"
@@ -140,7 +138,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\ReactiveUI.props = build\ReactiveUI.props
build\Rx.props = build\Rx.props
build\SampleApp.props = build\SampleApp.props
- build\Serilog.props = build\Serilog.props
build\SharpDX.props = build\SharpDX.props
build\SkiaSharp.props = build\SkiaSharp.props
build\System.Memory.props = build\System.Memory.props
@@ -1027,30 +1024,6 @@ Global
{410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|iPhone.Build.0 = Release|Any CPU
{410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.AppStore|Any CPU.Build.0 = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.AppStore|iPhone.ActiveCfg = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.AppStore|iPhone.Build.0 = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Debug|iPhone.ActiveCfg = Debug|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Debug|iPhone.Build.0 = Debug|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Release|Any CPU.Build.0 = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Release|iPhone.ActiveCfg = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Release|iPhone.Build.0 = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
- {B61B66A3-B82D-4875-8001-89D3394FE0C9}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
diff --git a/NOTICE.md b/NOTICE.md
index 0e1d792e84..92fd725957 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -271,3 +271,35 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
+
+# Chromium
+
+https://github.com/chromium/chromium
+
+// Copyright 2015 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/build.sh b/build.sh
index 40b1c225a6..a40e00f815 100755
--- a/build.sh
+++ b/build.sh
@@ -67,6 +67,8 @@ else
fi
fi
+export PATH=$DOTNET_DIRECTORY:$PATH
+
echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)"
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]}
diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props
index 2b54ee3f56..d17eec0135 100644
--- a/build/CoreLibraries.props
+++ b/build/CoreLibraries.props
@@ -7,7 +7,6 @@
-
diff --git a/build/Serilog.props b/build/Serilog.props
deleted file mode 100644
index a814cf998d..0000000000
--- a/build/Serilog.props
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/build/readme.md b/build/readme.md
index 387afb3425..e147556b1c 100644
--- a/build/readme.md
+++ b/build/readme.md
@@ -9,8 +9,6 @@
-
-
@@ -22,4 +20,4 @@
```XML
-```
\ No newline at end of file
+```
diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h
index b86c679397..f9bfaf0b47 100644
--- a/native/Avalonia.Native/inc/avalonia-native.h
+++ b/native/Avalonia.Native/inc/avalonia-native.h
@@ -387,6 +387,9 @@ AVNCOM(IAvnClipboard, 0f) : IUnknown
virtual HRESULT SetText (char* type, void* utf8Text) = 0;
virtual HRESULT ObtainFormats(IAvnStringArray**ppv) = 0;
virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) = 0;
+ virtual HRESULT SetBytes(char* type, void* utf8Text, int len) = 0;
+ virtual HRESULT GetBytes(char* type, IAvnString**ppv) = 0;
+
virtual HRESULT Clear() = 0;
};
diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h
index 88bc4e6963..5d299374e5 100644
--- a/native/Avalonia.Native/src/OSX/AvnString.h
+++ b/native/Avalonia.Native/src/OSX/AvnString.h
@@ -12,4 +12,5 @@
extern IAvnString* CreateAvnString(NSString* string);
extern IAvnStringArray* CreateAvnStringArray(NSArray* array);
extern IAvnStringArray* CreateAvnStringArray(NSString* string);
+extern IAvnString* CreateByteArray(void* data, int len);
#endif /* AvnString_h */
diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm
index 6445a9fef1..00b748ef63 100644
--- a/native/Avalonia.Native/src/OSX/AvnString.mm
+++ b/native/Avalonia.Native/src/OSX/AvnString.mm
@@ -29,6 +29,13 @@ public:
memcpy((void*)_cstring, (void*)cstring, _length);
}
+ AvnStringImpl(void*ptr, int len)
+ {
+ _length = len;
+ _cstring = (const char*)malloc(_length);
+ memcpy((void*)_cstring, ptr, len);
+ }
+
virtual ~AvnStringImpl()
{
free((void*)_cstring);
@@ -114,3 +121,8 @@ IAvnStringArray* CreateAvnStringArray(NSString* string)
{
return new AvnStringArrayImpl(string);
}
+
+IAvnString* CreateByteArray(void* data, int len)
+{
+ return new AvnStringImpl(data, len);
+}
diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm
index 18d60d3853..116a08670e 100644
--- a/native/Avalonia.Native/src/OSX/clipboard.mm
+++ b/native/Avalonia.Native/src/OSX/clipboard.mm
@@ -82,6 +82,40 @@ public:
return S_OK;
}
+
+ virtual HRESULT SetBytes(char* type, void* bytes, int len) override
+ {
+ auto typeString = [NSString stringWithUTF8String:(const char*)type];
+ auto data = [NSData dataWithBytes:bytes length:len];
+ if(_item == nil)
+ [_pb setData:data forType:typeString];
+ else
+ [_item setData:data forType:typeString];
+ return S_OK;
+ }
+
+ virtual HRESULT GetBytes(char* type, IAvnString**ppv) override
+ {
+ *ppv = nil;
+ auto typeString = [NSString stringWithUTF8String:(const char*)type];
+ NSData*data;
+ @try
+ {
+ if(_item)
+ data = [_item dataForType:typeString];
+ else
+ data = [_pb dataForType:typeString];
+ if(data == nil)
+ return E_FAIL;
+ }
+ @catch(NSException* e)
+ {
+ return E_FAIL;
+ }
+ *ppv = CreateByteArray((void*)data.bytes, (int)data.length);
+ return S_OK;
+ }
+
virtual HRESULT Clear() override
{
diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index abfae3cf1e..f826df4da9 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/native/Avalonia.Native/src/OSX/window.mm
@@ -1107,7 +1107,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_lastPixelSize.Width = (int)fsize.width;
_lastPixelSize.Height = (int)fsize.height;
[self updateRenderTarget];
- _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]);
+
+ if(_parent != nullptr)
+ {
+ _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]);
+ }
[super viewDidChangeBackingProperties];
}
@@ -1166,7 +1170,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
auto modifiers = [self getModifiers:[event modifierFlags]];
[self becomeFirstResponder];
- _parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta);
+
+ if(_parent != nullptr)
+ {
+ _parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta);
+ }
+
[super mouseMoved:event];
}
@@ -1290,7 +1299,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
auto timestamp = [event timestamp] * 1000;
auto modifiers = [self getModifiers:[event modifierFlags]];
- _lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key);
+ if(_parent != nullptr)
+ {
+ _lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key);
+ }
}
- (BOOL)performKeyEquivalent:(NSEvent *)event
@@ -1381,7 +1393,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
{
if(!_lastKeyHandled)
{
- _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]);
+ if(_parent != nullptr)
+ {
+ _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]);
+ }
}
}
@@ -1677,7 +1692,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
{
[self showWindowMenuWithAppMenu];
- _parent->BaseEvents->Activated();
+ if(_parent != nullptr)
+ {
+ _parent->BaseEvents->Activated();
+ }
+
[super becomeKeyWindow];
}
}
@@ -1794,8 +1813,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void)windowDidMove:(NSNotification *)notification
{
AvnPoint position;
- _parent->GetPosition(&position);
- _parent->BaseEvents->PositionChanged(position);
+
+ if(_parent != nullptr)
+ {
+ _parent->GetPosition(&position);
+ _parent->BaseEvents->PositionChanged(position);
+ }
}
@end
diff --git a/readme.md b/readme.md
index 8ae3f1ad66..a9263d4816 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,17 @@ 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/BindingDemo/App.xaml.cs b/samples/BindingDemo/App.xaml.cs
index f2f44cd502..13875aeb21 100644
--- a/samples/BindingDemo/App.xaml.cs
+++ b/samples/BindingDemo/App.xaml.cs
@@ -1,10 +1,8 @@
using System;
using Avalonia;
using Avalonia.Controls;
-using Avalonia.Logging.Serilog;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
-using Serilog;
namespace BindingDemo
{
diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj
index ce33f42143..817023fd71 100644
--- a/samples/BindingDemo/BindingDemo.csproj
+++ b/samples/BindingDemo/BindingDemo.csproj
@@ -10,7 +10,6 @@
-
diff --git a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj
index 054de2a05f..1a112d0d7d 100644
--- a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj
+++ b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj
@@ -10,6 +10,5 @@
-
diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs
index 2a8d288614..b2df1953f5 100644
--- a/samples/ControlCatalog.Desktop/Program.cs
+++ b/samples/ControlCatalog.Desktop/Program.cs
@@ -2,10 +2,8 @@ using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
-using Avalonia.Logging.Serilog;
using Avalonia.Platform;
using Avalonia.ReactiveUI;
-using Serilog;
namespace ControlCatalog
{
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 2a5294cacc..5df8c1be64 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -1,13 +1,11 @@
using System;
-using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using Avalonia;
-using Avalonia.ReactiveUI;
using Avalonia.Dialogs;
-using Avalonia.OpenGL;
+using Avalonia.ReactiveUI;
namespace ControlCatalog.NetCore
{
@@ -69,7 +67,8 @@ namespace ControlCatalog.NetCore
})
.UseSkia()
.UseReactiveUI()
- .UseManagedSystemDialogs();
+ .UseManagedSystemDialogs()
+ .LogToDebug();
static void SilenceConsole()
{
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/ToggleSwitchPage.xaml b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml
new file mode 100644
index 0000000000..161ee2ee16
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml.cs b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml.cs
new file mode 100644
index 0000000000..66f7d14c7f
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public class ToggleSwitchPage : UserControl
+ {
+ public ToggleSwitchPage()
+ {
+ this.InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
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/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml
index 26d25a6266..1ee6bf7a29 100644
--- a/samples/ControlCatalog/SideBar.xaml
+++ b/samples/ControlCatalog/SideBar.xaml
@@ -67,6 +67,9 @@
+
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/App.xaml.cs b/samples/RenderDemo/App.xaml.cs
index 923174d4c7..233160b025 100644
--- a/samples/RenderDemo/App.xaml.cs
+++ b/samples/RenderDemo/App.xaml.cs
@@ -1,5 +1,4 @@
using Avalonia;
-using Avalonia.Logging.Serilog;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
@@ -22,6 +21,5 @@ namespace RenderDemo
.UsePlatformDetect()
.UseReactiveUI()
.LogToDebug();
-
}
}
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/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj
index 0d7d62e177..d1654f4b54 100644
--- a/samples/RenderDemo/RenderDemo.csproj
+++ b/samples/RenderDemo/RenderDemo.csproj
@@ -13,7 +13,6 @@
-
diff --git a/samples/VirtualizationDemo/Program.cs b/samples/VirtualizationDemo/Program.cs
index 868a5e2640..93ea5e1b88 100644
--- a/samples/VirtualizationDemo/Program.cs
+++ b/samples/VirtualizationDemo/Program.cs
@@ -1,9 +1,7 @@
using System;
using Avalonia;
using Avalonia.Controls;
-using Avalonia.Logging.Serilog;
using Avalonia.ReactiveUI;
-using Serilog;
namespace VirtualizationDemo
{
diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj
index ce33f42143..817023fd71 100644
--- a/samples/VirtualizationDemo/VirtualizationDemo.csproj
+++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj
@@ -10,7 +10,6 @@
-
diff --git a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
index 3e318278c1..bd6b6f170f 100644
--- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
+++ b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
@@ -26,7 +26,6 @@
-
diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
index 51e0a1e799..7802f336fb 100644
--- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
@@ -43,5 +43,11 @@ namespace Avalonia.Android.Platform
return Task.FromResult(null);
}
+
+ public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException();
+
+ public Task GetFormatsAsync() => throw new PlatformNotSupportedException();
+
+ public Task GetDataAsync(string format) => throw new PlatformNotSupportedException();
}
}
diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
index 2f95a6e4bd..b8697e0ca2 100644
--- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
+++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
@@ -149,7 +149,6 @@
-
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/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index f387d7e0b6..d18f0b3f94 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -421,8 +421,7 @@ namespace Avalonia
throw new ArgumentException($"The property {property.Name} is readonly.");
}
- Logger.TryGet(LogEventLevel.Verbose)?.Log(
- LogArea.Property,
+ Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log(
this,
"Bound {Property} to {Binding} with priority LocalValue",
property,
@@ -501,8 +500,7 @@ namespace Avalonia
if (change.IsEffectiveValueChange)
{
- Logger.TryGet(LogEventLevel.Verbose)?.Log(
- LogArea.Property,
+ Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log(
this,
"{Property} changed from {$Old} to {$Value} with priority {Priority}",
property,
@@ -586,8 +584,7 @@ namespace Avalonia
/// The binding error.
protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e)
{
- Logger.TryGet(LogEventLevel.Warning)?.Log(
- LogArea.Binding,
+ Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log(
this,
"Error in binding to {Target}.{Property}: {Message}",
this,
@@ -857,8 +854,7 @@ namespace Avalonia
/// The priority.
private void LogPropertySet(AvaloniaProperty property, T value, BindingPriority priority)
{
- Logger.TryGet(LogEventLevel.Verbose)?.Log(
- LogArea.Property,
+ Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log(
this,
"Set {Property} to {$Value} with priority {Priority}",
property,
diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs
index 9721369e47..c4f61dfedb 100644
--- a/src/Avalonia.Base/Data/Core/BindingExpression.cs
+++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs
@@ -168,8 +168,7 @@ namespace Avalonia.Data.Core
}
else
{
- Logger.TryGet(LogEventLevel.Error)?.Log(
- LogArea.Binding,
+ Logger.TryGet(LogEventLevel.Error, LogArea.Binding)?.Log(
this,
"Could not convert FallbackValue {FallbackValue} to {Type}",
_fallbackValue,
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/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
index c19ee8dba7..5d694f4cf9 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
@@ -1,13 +1,16 @@
using System;
-using System.Linq;
+using System.Collections.Generic;
+using System.Linq.Expressions;
using System.Reflection;
namespace Avalonia.Data.Core.Plugins
{
- class MethodAccessorPlugin : IPropertyAccessorPlugin
+ public class MethodAccessorPlugin : IPropertyAccessorPlugin
{
- public bool Match(object obj, string methodName)
- => obj.GetType().GetRuntimeMethods().Any(x => x.Name == methodName);
+ private readonly Dictionary<(Type, string), MethodInfo> _methodLookup =
+ new Dictionary<(Type, string), MethodInfo>();
+
+ public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null;
public IPropertyAccessor Start(WeakReference reference, string methodName)
{
@@ -15,17 +18,22 @@ namespace Avalonia.Data.Core.Plugins
Contract.Requires(methodName != null);
reference.TryGetTarget(out object instance);
- var method = instance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == methodName);
+
+ var method = GetFirstMethodWithName(instance.GetType(), methodName);
if (method != null)
{
- if (method.GetParameters().Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8)
+ var parameters = method.GetParameters();
+
+ if (parameters.Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8)
{
- var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(methodName));
+ var exception = new ArgumentException(
+ "Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.",
+ nameof(methodName));
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
- return new Accessor(reference, method);
+ return new Accessor(reference, method, parameters);
}
else
{
@@ -35,31 +43,72 @@ namespace Avalonia.Data.Core.Plugins
}
}
+ private MethodInfo GetFirstMethodWithName(Type type, string methodName)
+ {
+ var key = (type, methodName);
+
+ if (!_methodLookup.TryGetValue(key, out MethodInfo methodInfo))
+ {
+ methodInfo = TryFindAndCacheMethod(type, methodName);
+ }
+
+ return methodInfo;
+ }
+
+ private MethodInfo TryFindAndCacheMethod(Type type, string methodName)
+ {
+ MethodInfo found = null;
+
+ const BindingFlags bindingFlags =
+ BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
+
+ var methods = type.GetMethods(bindingFlags);
+
+ foreach (MethodInfo methodInfo in methods)
+ {
+ if (methodInfo.Name == methodName)
+ {
+ found = methodInfo;
+
+ break;
+ }
+ }
+
+ _methodLookup.Add((type, methodName), found);
+
+ return found;
+ }
+
private sealed class Accessor : PropertyAccessorBase
{
- public Accessor(WeakReference reference, MethodInfo method)
+ public Accessor(WeakReference reference, MethodInfo method, ParameterInfo[] parameters)
{
Contract.Requires(reference != null);
Contract.Requires(method != null);
- var paramTypes = method.GetParameters().Select(param => param.ParameterType).ToArray();
var returnType = method.ReturnType;
-
- if (returnType == typeof(void))
+ bool hasReturn = returnType != typeof(void);
+
+ var signatureTypeCount = (hasReturn ? 1 : 0) + parameters.Length;
+
+ var paramTypes = new Type[signatureTypeCount];
+
+ for (var i = 0; i < parameters.Length; i++)
{
- if (paramTypes.Length == 0)
- {
- PropertyType = typeof(Action);
- }
- else
- {
- PropertyType = Type.GetType($"System.Action`{paramTypes.Length}").MakeGenericType(paramTypes);
- }
+ ParameterInfo parameter = parameters[i];
+
+ paramTypes[i] = parameter.ParameterType;
+ }
+
+ if (hasReturn)
+ {
+ paramTypes[paramTypes.Length - 1] = returnType;
+
+ PropertyType = Expression.GetFuncType(paramTypes);
}
else
{
- var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray();
- PropertyType = Type.GetType($"System.Func`{genericTypeParameters.Length}").MakeGenericType(genericTypeParameters);
+ PropertyType = Expression.GetActionType(paramTypes);
}
if (method.IsStatic)
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/Logging/DebugLogSink.cs b/src/Avalonia.Base/Logging/DebugLogSink.cs
new file mode 100644
index 0000000000..3695afa860
--- /dev/null
+++ b/src/Avalonia.Base/Logging/DebugLogSink.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using Avalonia.Utilities;
+
+namespace Avalonia.Logging
+{
+ public class DebugLogSink : ILogSink
+ {
+ private readonly LogEventLevel _level;
+ private readonly IList _areas;
+
+ public DebugLogSink(
+ LogEventLevel minimumLevel,
+ IList areas = null)
+ {
+ _level = minimumLevel;
+ _areas = areas?.Count > 0 ? areas : null;
+ }
+
+ public bool IsEnabled(LogEventLevel level, string area)
+ {
+ return level >= _level && (_areas?.Contains(area) ?? true);
+ }
+
+ public void Log(LogEventLevel level, string area, object source, string messageTemplate)
+ {
+ if (IsEnabled(level, area))
+ {
+ Debug.WriteLine(Format(area, messageTemplate, source));
+ }
+ }
+
+ public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0)
+ {
+ if (IsEnabled(level, area))
+ {
+ Debug.WriteLine(Format(area, messageTemplate, source, propertyValue0));
+ }
+ }
+
+ public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1)
+ {
+ if (IsEnabled(level, area))
+ {
+ Debug.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1));
+ }
+ }
+
+ public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2)
+ {
+ if (IsEnabled(level, area))
+ {
+ Debug.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1, propertyValue2));
+ }
+ }
+
+ public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues)
+ {
+ if (IsEnabled(level, area))
+ {
+ Debug.WriteLine(Format(area, messageTemplate, source, propertyValues));
+ }
+ }
+
+ private static string Format(
+ string area,
+ string template,
+ object source,
+ T0 v0 = default,
+ T1 v1 = default,
+ T2 v2 = default)
+ {
+ var result = new StringBuilder(template.Length);
+ var r = new CharacterReader(template.AsSpan());
+ var i = 0;
+
+ result.Append('[');
+ result.Append(area);
+ result.Append("] ");
+
+ while (!r.End)
+ {
+ var c = r.Take();
+
+ if (c != '{')
+ {
+ result.Append(c);
+ }
+ else
+ {
+ if (r.Peek != '{')
+ {
+ result.Append('\'');
+ result.Append(i++ switch
+ {
+ 0 => v0,
+ 1 => v1,
+ 2 => v2,
+ _ => null
+ });
+ result.Append('\'');
+ r.TakeUntil('}');
+ r.Take();
+ }
+ else
+ {
+ result.Append('{');
+ r.Take();
+ }
+ }
+ }
+
+ if (source is object)
+ {
+ result.Append(" (");
+ result.Append(source.GetType().Name);
+ result.Append(" #");
+ result.Append(source.GetHashCode());
+ result.Append(')');
+ }
+
+ return result.ToString();
+ }
+
+ private static string Format(
+ string area,
+ string template,
+ object source,
+ object[] v)
+ {
+ var result = new StringBuilder(template.Length);
+ var r = new CharacterReader(template.AsSpan());
+ var i = 0;
+
+ result.Append('[');
+ result.Append(area);
+ result.Append(']');
+
+ while (!r.End)
+ {
+ var c = r.Take();
+
+ if (c != '{')
+ {
+ result.Append(c);
+ }
+ else
+ {
+ if (r.Peek != '{')
+ {
+ result.Append('\'');
+ result.Append(i < v.Length ? v[i++] : null);
+ result.Append('\'');
+ r.TakeUntil('}');
+ r.Take();
+ }
+ else
+ {
+ result.Append('{');
+ r.Take();
+ }
+ }
+ }
+
+ if (source is object)
+ {
+ result.Append('(');
+ result.Append(source.GetType().Name);
+ result.Append(" #");
+ result.Append(source.GetHashCode());
+ result.Append(')');
+ }
+
+ return result.ToString();
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Logging/ILogSink.cs b/src/Avalonia.Base/Logging/ILogSink.cs
index 1649679b95..71268d5965 100644
--- a/src/Avalonia.Base/Logging/ILogSink.cs
+++ b/src/Avalonia.Base/Logging/ILogSink.cs
@@ -6,11 +6,12 @@ namespace Avalonia.Logging
public interface ILogSink
{
///
- /// Checks if given log level is enabled.
+ /// Checks if given log level and area is enabled.
///
/// The log event level.
+ /// The log area.
/// if given log level is enabled.
- bool IsEnabled(LogEventLevel level);
+ bool IsEnabled(LogEventLevel level, string area);
///
/// Logs an event.
diff --git a/src/Avalonia.Base/Logging/Logger.cs b/src/Avalonia.Base/Logging/Logger.cs
index 136f56a620..ed3fad93fc 100644
--- a/src/Avalonia.Base/Logging/Logger.cs
+++ b/src/Avalonia.Base/Logging/Logger.cs
@@ -14,36 +14,39 @@ namespace Avalonia.Logging
/// Checks if given log level is enabled.
///
/// The log event level.
+ /// The log area.
/// if given log level is enabled.
- public static bool IsEnabled(LogEventLevel level)
+ public static bool IsEnabled(LogEventLevel level, string area)
{
- return Sink?.IsEnabled(level) == true;
+ return Sink?.IsEnabled(level, area) == true;
}
///
/// Returns parametrized logging sink if given log level is enabled.
///
/// The log event level.
+ /// The area that the event originates from.
/// Log sink or if log level is not enabled.
- public static ParametrizedLogger? TryGet(LogEventLevel level)
+ public static ParametrizedLogger? TryGet(LogEventLevel level, string area)
{
- if (!IsEnabled(level))
+ if (!IsEnabled(level, area))
{
return null;
}
- return new ParametrizedLogger(Sink, level);
+ return new ParametrizedLogger(Sink, level, area);
}
///
/// Returns parametrized logging sink if given log level is enabled.
///
/// The log event level.
+ /// The area that the event originates from.
/// Log sink that is valid only if method returns .
/// if logger was obtained successfully.
- public static bool TryGet(LogEventLevel level, out ParametrizedLogger outLogger)
+ public static bool TryGet(LogEventLevel level, string area, out ParametrizedLogger outLogger)
{
- ParametrizedLogger? logger = TryGet(level);
+ ParametrizedLogger? logger = TryGet(level, area);
outLogger = logger.GetValueOrDefault();
diff --git a/src/Avalonia.Base/Logging/ParametrizedLogger.cs b/src/Avalonia.Base/Logging/ParametrizedLogger.cs
index 3dfb3c1ecf..adadb0f990 100644
--- a/src/Avalonia.Base/Logging/ParametrizedLogger.cs
+++ b/src/Avalonia.Base/Logging/ParametrizedLogger.cs
@@ -9,11 +9,13 @@ namespace Avalonia.Logging
{
private readonly ILogSink _sink;
private readonly LogEventLevel _level;
+ private readonly string _area;
- public ParametrizedLogger(ILogSink sink, LogEventLevel level)
+ public ParametrizedLogger(ILogSink sink, LogEventLevel level, string area)
{
_sink = sink;
_level = level;
+ _area = area;
}
///
@@ -24,58 +26,51 @@ namespace Avalonia.Logging
///
/// Logs an event.
///
- /// The area that the event originates.
/// The object from which the event originates.
/// The message template.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(
- string area,
object source,
string messageTemplate)
{
- _sink.Log(_level, area, source, messageTemplate);
+ _sink.Log(_level, _area, source, messageTemplate);
}
///
/// Logs an event.
///
- /// The area that the event originates.
/// The object from which the event originates.
/// The message template.
/// Message property value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(
- string area,
object source,
string messageTemplate,
T0 propertyValue0)
{
- _sink.Log(_level, area, source, messageTemplate, propertyValue0);
+ _sink.Log(_level, _area, source, messageTemplate, propertyValue0);
}
///
/// Logs an event.
///
- /// The area that the event originates.
/// The object from which the event originates.
/// The message template.
/// Message property value.
/// Message property value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(
- string area,
object source,
string messageTemplate,
T0 propertyValue0,
T1 propertyValue1)
{
- _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1);
+ _sink.Log(_level, _area, source, messageTemplate, propertyValue0, propertyValue1);
}
///
/// Logs an event.
///
- /// The area that the event originates.
/// The object from which the event originates.
/// The message template.
/// Message property value.
@@ -83,20 +78,18 @@ namespace Avalonia.Logging
/// Message property value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(
- string area,
object source,
string messageTemplate,
T0 propertyValue0,
T1 propertyValue1,
T2 propertyValue2)
{
- _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2);
+ _sink.Log(_level, _area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2);
}
///
/// Logs an event.
///
- /// The area that the event originates.
/// The object from which the event originates.
/// The message template.
/// Message property value.
@@ -105,7 +98,6 @@ namespace Avalonia.Logging
/// Message property value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(
- string area,
object source,
string messageTemplate,
T0 propertyValue0,
@@ -113,13 +105,12 @@ namespace Avalonia.Logging
T2 propertyValue2,
T3 propertyValue3)
{
- _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3);
+ _sink.Log(_level, _area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3);
}
///
/// Logs an event.
///
- /// The area that the event originates.
/// The object from which the event originates.
/// The message template.
/// Message property value.
@@ -129,7 +120,6 @@ namespace Avalonia.Logging
/// Message property value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(
- string area,
object source,
string messageTemplate,
T0 propertyValue0,
@@ -138,13 +128,12 @@ namespace Avalonia.Logging
T3 propertyValue3,
T4 propertyValue4)
{
- _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3, propertyValue4);
+ _sink.Log(_level, _area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3, propertyValue4);
}
///
/// Logs an event.
///
- /// The area that the event originates.
/// The object from which the event originates.
/// The message template.
/// Message property value.
@@ -155,7 +144,6 @@ namespace Avalonia.Logging
/// Message property value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(
- string area,
object source,
string messageTemplate,
T0 propertyValue0,
@@ -165,7 +153,7 @@ namespace Avalonia.Logging
T4 propertyValue4,
T5 propertyValue5)
{
- _sink.Log(_level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3, propertyValue4, propertyValue5);
+ _sink.Log(_level, _area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2, propertyValue3, propertyValue4, propertyValue5);
}
}
}
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/Reactive/TypedBindingAdapter.cs b/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs
index bd9b31b100..b99cef0f51 100644
--- a/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs
+++ b/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs
@@ -32,8 +32,7 @@ namespace Avalonia.Reactive
}
catch (InvalidCastException e)
{
- Logger.TryGet(LogEventLevel.Error)?.Log(
- LogArea.Binding,
+ Logger.TryGet(LogEventLevel.Error, LogArea.Binding)?.Log(
_target,
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
_property.Name,
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/Border.cs b/src/Avalonia.Controls/Border.cs
index b355310244..c8337df99c 100644
--- a/src/Avalonia.Controls/Border.cs
+++ b/src/Avalonia.Controls/Border.cs
@@ -1,13 +1,14 @@
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.Media;
+using Avalonia.VisualTree;
namespace Avalonia.Controls
{
///
/// A control which decorates a child with a border and background.
///
- public partial class Border : Decorator
+ public partial class Border : Decorator, IVisualWithRoundRectClip
{
///
/// Defines the property.
@@ -129,5 +130,7 @@ namespace Avalonia.Controls
{
return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness);
}
+
+ public CornerRadius ClipToBoundsRadius => CornerRadius;
}
}
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/DropDown.cs b/src/Avalonia.Controls/DropDown.cs
index 9da803d16d..4e17f5bff5 100644
--- a/src/Avalonia.Controls/DropDown.cs
+++ b/src/Avalonia.Controls/DropDown.cs
@@ -9,7 +9,7 @@ namespace Avalonia.Controls
{
public DropDown()
{
- Logger.TryGet(LogEventLevel.Warning)?.Log(LogArea.Control, this, "DropDown is deprecated: Use ComboBox");
+ Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "DropDown is deprecated: Use ComboBox");
}
Type IStyleable.StyleKey => typeof(ComboBox);
@@ -20,7 +20,7 @@ namespace Avalonia.Controls
{
public DropDownItem()
{
- Logger.TryGet(LogEventLevel.Warning)?.Log(LogArea.Control, this, "DropDownItem is deprecated: Use ComboBoxItem");
+ Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "DropDownItem is deprecated: Use ComboBoxItem");
}
Type IStyleable.StyleKey => typeof(ComboBoxItem);
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/LoggingExtensions.cs b/src/Avalonia.Controls/LoggingExtensions.cs
new file mode 100644
index 0000000000..44e570bdfa
--- /dev/null
+++ b/src/Avalonia.Controls/LoggingExtensions.cs
@@ -0,0 +1,26 @@
+using Avalonia.Controls;
+using Avalonia.Logging;
+
+namespace Avalonia
+{
+ public static class LoggingExtensions
+ {
+ ///
+ /// Logs Avalonia events to the sink.
+ ///
+ /// The application class type.
+ /// The app builder instance.
+ /// The minimum level to log.
+ /// The areas to log. Valid values are listed in .
+ /// The app builder instance.
+ public static T LogToDebug(
+ this T builder,
+ LogEventLevel level = LogEventLevel.Warning,
+ params string[] areas)
+ where T : AppBuilderBase, new()
+ {
+ Logger.Sink = new DebugLogSink(level, areas);
+ return builder;
+ }
+ }
+}
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/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
index 820d5777f5..d18cf7da71 100644
--- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs
+++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
@@ -252,7 +252,7 @@ namespace Avalonia.Controls.Primitives
if (template != null)
{
- Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Control, this, "Creating control template");
+ Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log(this, "Creating control template");
var (child, nameScope) = template.Build(this);
ApplyTemplatedParent(child);
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 cd0e7a1601..bc30e6a678 100644
--- a/src/Avalonia.Controls/ScrollViewer.cs
+++ b/src/Avalonia.Controls/ScrollViewer.cs
@@ -384,6 +384,38 @@ namespace Avalonia.Controls
get { return _viewport.Height; }
}
+ ///
+ /// 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.
///
@@ -587,16 +619,19 @@ namespace Avalonia.Controls
private void RaiseScrollChanged()
{
- var e = new ScrollChangedEventArgs(
- new Vector(Extent.Width - _oldExtent.Width, Extent.Height - _oldExtent.Height),
- Offset - _oldOffset,
- new Vector(Viewport.Width - _oldViewport.Width, Viewport.Height - _oldViewport.Height));
+ var extentDelta = new Vector(Extent.Width - _oldExtent.Width, Extent.Height - _oldExtent.Height);
+ var offsetDelta = Offset - _oldOffset;
+ var viewportDelta = new Vector(Viewport.Width - _oldViewport.Width, Viewport.Height - _oldViewport.Height);
- OnScrollChanged(e);
+ if (!extentDelta.NearlyEquals(default) || !offsetDelta.NearlyEquals(default) || !viewportDelta.NearlyEquals(default))
+ {
+ var e = new ScrollChangedEventArgs(extentDelta, offsetDelta, viewportDelta);
+ OnScrollChanged(e);
- _oldExtent = Extent;
- _oldOffset = Offset;
- _oldViewport = Viewport;
+ _oldExtent = Extent;
+ _oldOffset = Offset;
+ _oldViewport = Viewport;
+ }
}
}
}
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/TabItem.cs b/src/Avalonia.Controls/TabItem.cs
index 038ea225ce..6320443a13 100644
--- a/src/Avalonia.Controls/TabItem.cs
+++ b/src/Avalonia.Controls/TabItem.cs
@@ -26,6 +26,7 @@ namespace Avalonia.Controls
static TabItem()
{
SelectableMixin.Attach(IsSelectedProperty);
+ PressedMixin.Attach();
FocusableProperty.OverrideDefaultValue(typeof(TabItem), true);
DataContextProperty.Changed.AddClassHandler((x, e) => x.UpdateHeader(e));
}
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..4b42c574cf
--- /dev/null
+++ b/src/Avalonia.Controls/ToggleSwitch.cs
@@ -0,0 +1,136 @@
+using Avalonia.Controls.Presenters;
+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); }
+ }
+
+ public IContentPresenter OffContentPresenter
+ {
+ get;
+ private set;
+ }
+
+ public IContentPresenter OnContentPresenter
+ {
+ get;
+ private set;
+ }
+
+ ///
+ /// 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);
+ }
+ }
+
+ protected override bool RegisterContentPresenter(IContentPresenter presenter)
+ {
+ var result = base.RegisterContentPresenter(presenter);
+
+ if (presenter.Name == "Part_OnContentPresenter")
+ {
+ OnContentPresenter = presenter;
+ result = true;
+ }
+ else if (presenter.Name == "PART_OffContentPresenter")
+ {
+ OffContentPresenter = presenter;
+ result = true;
+ }
+
+ return result;
+ }
+ }
+}
+
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..5d34444eb8 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;
@@ -452,8 +449,7 @@ namespace Avalonia.Controls
if (result == null)
{
- Logger.TryGet(LogEventLevel.Warning)?.Log(
- LogArea.Control,
+ Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(
this,
"Could not create {Service} : maybe Application.RegisterServices() wasn't called?",
typeof(T));
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 c2565cc59c..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; }
@@ -160,6 +160,10 @@ namespace Avalonia.DesignerSupport.Remote
public Task SetTextAsync(string text) => Task.CompletedTask;
public Task ClearAsync() => Task.CompletedTask;
+ public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
+ public Task GetFormatsAsync() => Task.FromResult(new string[0]);
+
+ public Task GetDataAsync(string format) => Task.FromResult((object)null);
}
class CursorFactoryStub : IStandardCursorFactory
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.Input/Platform/IClipboard.cs b/src/Avalonia.Input/Platform/IClipboard.cs
index b195050fa8..eb880904eb 100644
--- a/src/Avalonia.Input/Platform/IClipboard.cs
+++ b/src/Avalonia.Input/Platform/IClipboard.cs
@@ -9,5 +9,11 @@ namespace Avalonia.Input.Platform
Task SetTextAsync(string text);
Task ClearAsync();
+
+ Task SetDataObjectAsync(IDataObject data);
+
+ Task GetFormatsAsync();
+
+ Task GetDataAsync(string format);
}
}
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.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs
index 908758045a..1c0c736b78 100644
--- a/src/Avalonia.Layout/LayoutManager.cs
+++ b/src/Avalonia.Layout/LayoutManager.cs
@@ -80,12 +80,11 @@ namespace Avalonia.Layout
Stopwatch? stopwatch = null;
const LogEventLevel timingLogLevel = LogEventLevel.Information;
- bool captureTiming = Logger.IsEnabled(timingLogLevel);
+ bool captureTiming = Logger.IsEnabled(timingLogLevel, LogArea.Layout);
if (captureTiming)
{
- Logger.TryGet(timingLogLevel)?.Log(
- LogArea.Layout,
+ Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(
this,
"Started layout pass. To measure: {Measure} To arrange: {Arrange}",
_toMeasure.Count,
@@ -123,7 +122,7 @@ namespace Avalonia.Layout
{
stopwatch!.Stop();
- Logger.TryGet(timingLogLevel)?.Log(LogArea.Layout, this, "Layout pass finished in {Time}", stopwatch.Elapsed);
+ Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", stopwatch.Elapsed);
}
}
diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs
index 513d3d540e..8d2a825fa0 100644
--- a/src/Avalonia.Layout/Layoutable.cs
+++ b/src/Avalonia.Layout/Layoutable.cs
@@ -349,7 +349,7 @@ namespace Avalonia.Layout
DesiredSize = desiredSize;
_previousMeasure = availableSize;
- Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Measure requested {DesiredSize}", DesiredSize);
+ Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Measure requested {DesiredSize}", DesiredSize);
if (DesiredSize != previousDesiredSize)
{
@@ -376,7 +376,7 @@ namespace Avalonia.Layout
if (!IsArrangeValid || _previousArrange != rect)
{
- Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Arrange to {Rect} ", rect);
+ Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Arrange to {Rect} ", rect);
IsArrangeValid = true;
ArrangeCore(rect);
@@ -398,7 +398,7 @@ namespace Avalonia.Layout
{
if (IsMeasureValid)
{
- Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Invalidated measure");
+ Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Invalidated measure");
IsMeasureValid = false;
IsArrangeValid = false;
@@ -419,7 +419,7 @@ namespace Avalonia.Layout
{
if (IsArrangeValid)
{
- Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Layout, this, "Invalidated arrange");
+ Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Invalidated arrange");
IsArrangeValid = false;
(VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this);
diff --git a/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj b/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj
deleted file mode 100644
index 3e76001556..0000000000
--- a/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
- netstandard2.0
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Logging.Serilog/SerilogExtensions.cs b/src/Avalonia.Logging.Serilog/SerilogExtensions.cs
deleted file mode 100644
index 2ce38769da..0000000000
--- a/src/Avalonia.Logging.Serilog/SerilogExtensions.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-using System;
-using Avalonia.Controls;
-using Serilog;
-using Serilog.Configuration;
-using Serilog.Filters;
-using SerilogLevel = Serilog.Events.LogEventLevel;
-
-namespace Avalonia.Logging.Serilog
-{
- ///
- /// Extension methods for Serilog logging.
- ///
- public static class SerilogExtensions
- {
- private const string DefaultTemplate = "[{Area}] {Message} ({SourceType} #{SourceHash})";
-
- ///
- /// Logs Avalonia events to the sink.
- ///
- /// The application class type.
- /// The app builder instance.
- /// The minimum level to log.
- /// The app builder instance.
- public static T LogToDebug(
- this T builder,
- LogEventLevel level = LogEventLevel.Warning)
- where T : AppBuilderBase, new()
- {
- SerilogLogger.Initialize(new LoggerConfiguration()
- .MinimumLevel.Is((SerilogLevel)level)
- .Enrich.FromLogContext()
- .WriteTo.Debug(outputTemplate: DefaultTemplate)
- .CreateLogger());
- return builder;
- }
-
- ///
- /// Logs Avalonia events to the sink.
- ///
- /// The application class type.
- /// The app builder instance.
- /// The area to log. Valid values are listed in .
- /// The minimum level to log.
- /// The app builder instance.
- public static T LogToDebug(
- this T builder,
- string area,
- LogEventLevel level = LogEventLevel.Warning)
- where T : AppBuilderBase, new()
- {
- SerilogLogger.Initialize(new LoggerConfiguration()
- .MinimumLevel.Is((SerilogLevel)level)
- .Filter.ByIncludingOnly(Matching.WithProperty("Area", area))
- .Enrich.FromLogContext()
- .WriteTo.Debug(outputTemplate: DefaultTemplate)
- .CreateLogger());
- return builder;
- }
-
- ///
- /// Logs Avalonia events to the sink.
- ///
- /// The application class type.
- /// The app builder instance.
- /// The minimum level to log.
- /// The app builder instance.
- public static T LogToTrace(
- this T builder,
- LogEventLevel level = LogEventLevel.Warning)
- where T : AppBuilderBase, new()
- {
- SerilogLogger.Initialize(new LoggerConfiguration()
- .MinimumLevel.Is((SerilogLevel)level)
- .Enrich.FromLogContext()
- .WriteTo.Trace(outputTemplate: DefaultTemplate)
- .CreateLogger());
- return builder;
- }
-
- ///
- /// Logs Avalonia events to the sink.
- ///
- /// The application class type.
- /// The app builder instance.
- /// The area to log. Valid values are listed in .
- /// The minimum level to log.
- /// The app builder instance.
- public static T LogToTrace(
- this T builder,
- string area,
- LogEventLevel level = LogEventLevel.Warning)
- where T : AppBuilderBase, new()
- {
- SerilogLogger.Initialize(new LoggerConfiguration()
- .MinimumLevel.Is((SerilogLevel)level)
- .Filter.ByIncludingOnly(Matching.WithProperty("Area", area))
- .Enrich.FromLogContext()
- .WriteTo.Trace(outputTemplate: DefaultTemplate)
- .CreateLogger());
- return builder;
- }
- }
-}
diff --git a/src/Avalonia.Logging.Serilog/SerilogLogger.cs b/src/Avalonia.Logging.Serilog/SerilogLogger.cs
deleted file mode 100644
index e51925a0f2..0000000000
--- a/src/Avalonia.Logging.Serilog/SerilogLogger.cs
+++ /dev/null
@@ -1,151 +0,0 @@
-using System;
-using Serilog;
-using Serilog.Context;
-using AvaloniaLogEventLevel = Avalonia.Logging.LogEventLevel;
-using SerilogLogEventLevel = Serilog.Events.LogEventLevel;
-
-namespace Avalonia.Logging.Serilog
-{
- ///
- /// Sends log output to serilog.
- ///
- public class SerilogLogger : ILogSink
- {
- private readonly ILogger _output;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The serilog logger to use.
- public SerilogLogger(ILogger output)
- {
- _output = output;
- }
-
- ///
- /// Initializes the Avalonia logging with a new instance of a .
- ///
- /// The serilog logger to use.
- public static void Initialize(ILogger output)
- {
- Logger.Sink = new SerilogLogger(output);
- }
-
- public bool IsEnabled(LogEventLevel level)
- {
- return _output.IsEnabled((SerilogLogEventLevel)level);
- }
-
- public void Log(
- LogEventLevel level,
- string area,
- object source,
- string messageTemplate)
- {
- Contract.Requires(area != null);
- Contract.Requires(messageTemplate != null);
-
- using (PushLogContextProperties(area, source))
- {
- _output.Write((SerilogLogEventLevel)level, messageTemplate);
- }
- }
-
- public void Log(
- LogEventLevel level,
- string area, object source,
- string messageTemplate,
- T0 propertyValue0)
- {
- Contract.Requires(area != null);
- Contract.Requires(messageTemplate != null);
-
- using (PushLogContextProperties(area, source))
- {
- _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0);
- }
- }
-
- public void Log(
- LogEventLevel level,
- string area,
- object source,
- string messageTemplate,
- T0 propertyValue0,
- T1 propertyValue1)
- {
- Contract.Requires(area != null);
- Contract.Requires(messageTemplate != null);
-
- using (PushLogContextProperties(area, source))
- {
- _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0, propertyValue1);
- }
- }
-
- public void Log(
- LogEventLevel level,
- string area,
- object source,
- string messageTemplate,
- T0 propertyValue0,
- T1 propertyValue1,
- T2 propertyValue2)
- {
- Contract.Requires(area != null);
- Contract.Requires(messageTemplate != null);
-
- using (PushLogContextProperties(area, source))
- {
- _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValue0, propertyValue1, propertyValue2);
- }
- }
-
- ///
- public void Log(
- AvaloniaLogEventLevel level,
- string area,
- object source,
- string messageTemplate,
- params object[] propertyValues)
- {
- Contract.Requires(area != null);
- Contract.Requires(messageTemplate != null);
-
- using (PushLogContextProperties(area, source))
- {
- _output.Write((SerilogLogEventLevel)level, messageTemplate, propertyValues);
- }
- }
-
- private static LogContextDisposable PushLogContextProperties(string area, object source)
- {
- return new LogContextDisposable(
- LogContext.PushProperty("Area", area),
- LogContext.PushProperty("SourceType", source?.GetType()),
- LogContext.PushProperty("SourceHash", source?.GetHashCode())
- );
- }
-
- private readonly struct LogContextDisposable : IDisposable
- {
- private readonly IDisposable _areaDisposable;
- private readonly IDisposable _sourceTypeDisposable;
- private readonly IDisposable _sourceHashDisposable;
-
- public LogContextDisposable(IDisposable areaDisposable, IDisposable sourceTypeDisposable, IDisposable sourceHashDisposable)
- {
- _areaDisposable = areaDisposable;
- _sourceTypeDisposable = sourceTypeDisposable;
- _sourceHashDisposable = sourceHashDisposable;
- }
-
- public void Dispose()
- {
- _areaDisposable.Dispose();
- _sourceTypeDisposable.Dispose();
- _sourceHashDisposable.Dispose();
- }
- }
- }
-}
diff --git a/src/Avalonia.Native/AvnString.cs b/src/Avalonia.Native/AvnString.cs
index ba427b6aac..11b1a33276 100644
--- a/src/Avalonia.Native/AvnString.cs
+++ b/src/Avalonia.Native/AvnString.cs
@@ -5,6 +5,7 @@ namespace Avalonia.Native.Interop
unsafe partial class IAvnString
{
private string _managed;
+ private byte[] _bytes;
public string String
{
@@ -22,6 +23,20 @@ namespace Avalonia.Native.Interop
}
}
+ public byte[] Bytes
+ {
+ get
+ {
+ if (_bytes == null)
+ {
+ _bytes = new byte[Length()];
+ Marshal.Copy(Pointer(), _bytes, 0, _bytes.Length);
+ }
+
+ return _bytes;
+ }
+ }
+
public override string ToString() => String;
}
diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs
index 2a24869513..554e7a497a 100644
--- a/src/Avalonia.Native/ClipboardImpl.cs
+++ b/src/Avalonia.Native/ClipboardImpl.cs
@@ -82,6 +82,38 @@ namespace Avalonia.Native
using (var strings = _native.GetStrings(NSFilenamesPboardType))
return strings.ToStringArray();
}
+
+ public unsafe Task SetDataObjectAsync(IDataObject data)
+ {
+ _native.Clear();
+ foreach (var fmt in data.GetDataFormats())
+ {
+ var o = data.Get(fmt);
+ if(o is string s)
+ using (var b = new Utf8Buffer(s))
+ _native.SetText(fmt, b.DangerousGetHandle());
+ else if(o is byte[] bytes)
+ fixed (byte* pbytes = bytes)
+ _native.SetBytes(fmt, new IntPtr(pbytes), bytes.Length);
+ }
+ return Task.CompletedTask;
+ }
+
+ public Task GetFormatsAsync()
+ {
+ using (var n = _native.ObtainFormats())
+ return Task.FromResult(n.ToStringArray());
+ }
+
+ public async Task GetDataAsync(string format)
+ {
+ if (format == DataFormats.Text)
+ return await GetTextAsync();
+ if (format == DataFormats.FileNames)
+ return GetFileNames();
+ using (var n = _native.GetBytes(format))
+ return n.Bytes;
+ }
}
class ClipboardDataObject : IDataObject, IDisposable
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.OpenGL/EglGlPlatformFeature.cs b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs
index cf3bce8756..f59c6b7751 100644
--- a/src/Avalonia.OpenGL/EglGlPlatformFeature.cs
+++ b/src/Avalonia.OpenGL/EglGlPlatformFeature.cs
@@ -34,7 +34,7 @@ namespace Avalonia.OpenGL
}
catch(Exception e)
{
- Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", null, "Unable to initialize EGL-based rendering: {0}", e);
+ Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e);
return null;
}
}
diff --git a/src/Avalonia.OpenGL/OpenGlControlBase.cs b/src/Avalonia.OpenGL/OpenGlControlBase.cs
index 6268c81516..8567dcae20 100644
--- a/src/Avalonia.OpenGL/OpenGlControlBase.cs
+++ b/src/Avalonia.OpenGL/OpenGlControlBase.cs
@@ -94,7 +94,7 @@ namespace Avalonia.OpenGL
}
catch (Exception e)
{
- Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", "OpenGlControlBase",
+ Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase",
"Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e);
_glFailed = true;
return false;
@@ -109,7 +109,7 @@ namespace Avalonia.OpenGL
{
_context.Dispose();
_context = null;
- Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", "OpenGlControlBase",
+ Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase",
"Unable to initialize OpenGL: unable to create OpenGlTextureBitmap: {exception}", e);
_glFailed = true;
return false;
@@ -138,7 +138,7 @@ namespace Avalonia.OpenGL
{
int code;
while ((code = gl.GetError()) != 0)
- Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", "OpenGlControlBase",
+ Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase",
"Unable to initialize OpenGL FBO: {code}", code);
_glFailed = true;
@@ -147,7 +147,7 @@ namespace Avalonia.OpenGL
}
catch(Exception e)
{
- Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", "OpenGlControlBase",
+ Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase",
"Unable to initialize OpenGL FBO: {exception}", e);
_glFailed = true;
}
diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs
index bdd01924f1..05e031c9ec 100644
--- a/src/Avalonia.Styling/StyledElement.cs
+++ b/src/Avalonia.Styling/StyledElement.cs
@@ -689,8 +689,7 @@ namespace Avalonia
#if DEBUG
if (((INotifyCollectionChangedDebug)Classes).GetCollectionChangedSubscribers()?.Length > 0)
{
- Logger.TryGet(LogEventLevel.Warning)?.Log(
- LogArea.Control,
+ Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(
this,
"{Type} detached from logical tree but still has class listeners",
GetType());
diff --git a/src/Avalonia.Themes.Fluent/DatePicker.xaml b/src/Avalonia.Themes.Default/CalendarDatePicker.xaml
similarity index 97%
rename from src/Avalonia.Themes.Fluent/DatePicker.xaml
rename to src/Avalonia.Themes.Default/CalendarDatePicker.xaml
index 7adb1c2d5f..bc1aba1a03 100644
--- a/src/Avalonia.Themes.Fluent/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..1ef9ed84c9 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..9a1ced3db8 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..b9502daaea 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..d16e6b6b55 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/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
index 3c3e14010d..a4eab83e4a 100644
--- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
+++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
@@ -19,4 +19,4 @@
-
+
\ No newline at end of file
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.Default/DatePicker.xaml b/src/Avalonia.Themes.Fluent/CalendarDatePicker.xaml
similarity index 97%
rename from src/Avalonia.Themes.Default/DatePicker.xaml
rename to src/Avalonia.Themes.Fluent/CalendarDatePicker.xaml
index 7adb1c2d5f..bc1aba1a03 100644
--- a/src/Avalonia.Themes.Default/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/TabControl.xaml b/src/Avalonia.Themes.Fluent/TabControl.xaml
index ed2e67df28..3cfdf60ae0 100644
--- a/src/Avalonia.Themes.Fluent/TabControl.xaml
+++ b/src/Avalonia.Themes.Fluent/TabControl.xaml
@@ -1,62 +1,61 @@
-
-
-
-
-
-
-
-
-
-
-
-
+ ItemTemplate="{TemplateBinding ItemTemplate}"
+ DockPanel.Dock="{TemplateBinding TabStripPlacement}"/>
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/TabItem.xaml b/src/Avalonia.Themes.Fluent/TabItem.xaml
index 92482a564c..694e86a184 100644
--- a/src/Avalonia.Themes.Fluent/TabItem.xaml
+++ b/src/Avalonia.Themes.Fluent/TabItem.xaml
@@ -1,45 +1,130 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ 48
+ 24
+ 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/TabStrip.xaml b/src/Avalonia.Themes.Fluent/TabStrip.xaml
index 29ca1e7c94..8b3c0a8310 100644
--- a/src/Avalonia.Themes.Fluent/TabStrip.xaml
+++ b/src/Avalonia.Themes.Fluent/TabStrip.xaml
@@ -1,11 +1,20 @@
+
+
+
+ Item 1
+ Item 2
+ Disabled
+
+
+
-
diff --git a/src/Avalonia.Themes.Fluent/TabStripItem.xaml b/src/Avalonia.Themes.Fluent/TabStripItem.xaml
index 28c4c68a3d..d45f705a40 100644
--- a/src/Avalonia.Themes.Fluent/TabStripItem.xaml
+++ b/src/Avalonia.Themes.Fluent/TabStripItem.xaml
@@ -1,23 +1,103 @@
-
+
+
+
+
+ Leaf
+ Arch
+
+
+
+
+ 48
+ 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
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..1b2142f6c9 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))
@@ -65,15 +73,13 @@ namespace Avalonia.Animation.Animators
}
}
- Logger.TryGet(LogEventLevel.Warning)?.Log(
- LogArea.Animations,
+ Logger.TryGet(LogEventLevel.Warning, LogArea.Animations)?.Log(
control,
$"Cannot find the appropriate transform: \"{Property.OwnerType}\" in {control}.");
}
else
{
- Logger.TryGet(LogEventLevel.Error)?.Log(
- LogArea.Animations,
+ Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log(
control,
$"Cannot apply animation: Target property owner {Property.OwnerType} is not a Transform object.");
}
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 6fdcd9631b..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);
}
@@ -282,6 +283,12 @@ namespace Avalonia.Media
}
+ public PushedState PushClip(RoundedRect clip)
+ {
+ PlatformImpl.PushClip(clip);
+ return new PushedState(this, PushedState.PushedStateType.Clip);
+ }
+
///
/// Pushes a clip rectangle.
///
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/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
index 660d10c088..c87946b3ea 100644
--- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
@@ -107,6 +107,12 @@ namespace Avalonia.Platform
/// The clip rectangle.
void PushClip(Rect clip);
+ ///
+ /// Pushes a clip rounded rectangle.
+ ///
+ /// The clip rounded rectangle
+ void PushClip(RoundedRect clip);
+
///
/// Pops the latest pushed clip rectangle.
///
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/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
index 59dd369956..8c020fc073 100644
--- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
@@ -287,7 +287,7 @@ namespace Avalonia.Rendering
}
catch (RenderTargetCorruptedException ex)
{
- Logger.TryGet(LogEventLevel.Information)?.Log("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
+ Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex);
RenderTarget?.Dispose();
RenderTarget = null;
}
diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
index c1c4b6bc99..9ea1b84311 100644
--- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
@@ -78,7 +78,7 @@ namespace Avalonia.Rendering
}
catch (RenderTargetCorruptedException ex)
{
- Logger.TryGet(LogEventLevel.Information)?.Log("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
+ Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex);
_renderTarget.Dispose();
_renderTarget = null;
}
@@ -289,7 +289,11 @@ namespace Avalonia.Rendering
using (context.PushPostTransform(m))
using (context.PushOpacity(opacity))
- using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState))
+ using (clipToBounds
+ ? visual is IVisualWithRoundRectClip roundClipVisual
+ ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius))
+ : context.PushClip(bounds)
+ : default(DrawingContext.PushedState))
using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState))
using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState))
using (context.PushTransformContainer())
diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs
index c2594658b9..789d028a3a 100644
--- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs
+++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs
@@ -120,7 +120,7 @@ namespace Avalonia.Rendering
}
catch (Exception ex)
{
- Logger.TryGet(LogEventLevel.Error)?.Log(LogArea.Visual, this, "Exception in render update: {Error}", ex);
+ Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception in render update: {Error}", ex);
}
}
}
@@ -136,7 +136,7 @@ namespace Avalonia.Rendering
}
catch (Exception ex)
{
- Logger.TryGet(LogEventLevel.Error)?.Log(LogArea.Visual, this, "Exception in render loop: {Error}", ex);
+ Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception in render loop: {Error}", ex);
}
finally
{
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs
index 34f042e334..ada04bfefd 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs
@@ -16,6 +16,16 @@ namespace Avalonia.Rendering.SceneGraph
{
Clip = clip;
}
+
+ ///
+ /// Initializes a new instance of the class that represents a
+ /// clip push.
+ ///
+ /// The clip to push.
+ public ClipNode(RoundedRect clip)
+ {
+ Clip = clip;
+ }
///
/// Initializes a new instance of the class that represents a
@@ -31,7 +41,7 @@ namespace Avalonia.Rendering.SceneGraph
///
/// Gets the clip to be pushed or null if the operation represents a pop.
///
- public Rect? Clip { get; }
+ public RoundedRect? Clip { get; }
///
public bool HitTest(Point p) => false;
@@ -45,7 +55,7 @@ namespace Avalonia.Rendering.SceneGraph
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
///
- public bool Equals(Rect? clip) => Clip == clip;
+ public bool Equals(RoundedRect? clip) => Clip == clip;
///
public void Render(IDrawingContextImpl context)
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
index 6ad71ac111..dfb21a0289 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
@@ -163,7 +163,7 @@ namespace Avalonia.Rendering.SceneGraph
++_drawOperationindex;
}
}
-
+
public void Custom(ICustomDrawOperation custom)
{
var next = NextDrawAs();
@@ -283,6 +283,21 @@ namespace Avalonia.Rendering.SceneGraph
}
}
+ ///
+ public void PushClip(RoundedRect clip)
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(clip))
+ {
+ Add(new ClipNode(clip));
+ }
+ else
+ {
+ ++_drawOperationindex;
+ }
+ }
+
///
public void PushGeometryClip(IGeometryImpl clip)
{
@@ -368,7 +383,7 @@ namespace Avalonia.Rendering.SceneGraph
{
using (var refCounted = RefCountable.Create(node))
{
- Add(refCounted);
+ Add(refCounted);
}
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
index 8b77c37c1f..6d12b5bca4 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
@@ -26,6 +26,11 @@ namespace Avalonia.Rendering.SceneGraph
///
Matrix Transform { get; }
+ ///
+ /// Gets the corner radius of visual. Contents are clipped to this radius.
+ ///
+ CornerRadius ClipToBoundsRadius { get; }
+
///
/// Gets the bounds of the node's geometry in global coordinates.
///
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
index 263a491933..5da44c5943 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
@@ -163,6 +163,10 @@ namespace Avalonia.Rendering.SceneGraph
var visual = node.Visual;
var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds;
+ var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ?
+ roundRectClip.ClipToBoundsRadius :
+ default;
+
var bounds = new Rect(visual.Bounds.Size);
var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl;
@@ -201,6 +205,7 @@ namespace Avalonia.Rendering.SceneGraph
node.ClipBounds = clipBounds;
node.ClipToBounds = clipToBounds;
node.LayoutBounds = globalBounds;
+ node.ClipToBoundsRadius = clipToBoundsRadius;
node.GeometryClip = visual.Clip?.PlatformImpl;
node.Opacity = opacity;
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
index 6f566ff6d6..8fb6b2542a 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
@@ -46,6 +46,9 @@ namespace Avalonia.Rendering.SceneGraph
///
public IVisualNode Parent { get; }
+ ///
+ public CornerRadius ClipToBoundsRadius { get; set; }
+
///
public Matrix Transform { get; set; }
@@ -262,6 +265,7 @@ namespace Avalonia.Rendering.SceneGraph
{
Transform = Transform,
ClipBounds = ClipBounds,
+ ClipToBoundsRadius = ClipToBoundsRadius,
ClipToBounds = ClipToBounds,
LayoutBounds = LayoutBounds,
GeometryClip = GeometryClip,
@@ -301,7 +305,10 @@ namespace Avalonia.Rendering.SceneGraph
if (ClipToBounds)
{
context.Transform = Matrix.Identity;
- context.PushClip(ClipBounds);
+ if (ClipToBoundsRadius.IsEmpty)
+ context.PushClip(ClipBounds);
+ else
+ context.PushClip(new RoundedRect(ClipBounds, ClipToBoundsRadius));
}
context.Transform = Transform;
diff --git a/src/Avalonia.Visuals/RoundedRect.cs b/src/Avalonia.Visuals/RoundedRect.cs
index ad860240f2..3452bc1ff8 100644
--- a/src/Avalonia.Visuals/RoundedRect.cs
+++ b/src/Avalonia.Visuals/RoundedRect.cs
@@ -27,6 +27,10 @@ namespace Avalonia
}
}
+ public static bool operator ==(RoundedRect left, RoundedRect right) => left.Equals(right);
+
+ public static bool operator !=(RoundedRect left, RoundedRect right) => !left.Equals(right);
+
public Rect Rect { get; }
public Vector RadiiTopLeft { get; }
public Vector RadiiTopRight { get; }
@@ -74,6 +78,13 @@ namespace Avalonia
}
+ public RoundedRect(in Rect bounds, in CornerRadius radius) : this(bounds,
+ radius.TopLeft, radius.TopRight,
+ radius.BottomRight, radius.BottomLeft)
+ {
+
+ }
+
public static implicit operator RoundedRect(Rect r) => new RoundedRect(r);
public bool IsRounded => RadiiTopLeft != default || RadiiTopRight != default || RadiiBottomRight != default ||
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..cd6eb6aac7 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); }
@@ -387,13 +387,13 @@ namespace Avalonia
/// The event args.
protected virtual void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
- Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Visual, this, "Attached to visual tree");
+ Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Attached to visual tree");
_visualRoot = e.Root;
- if (RenderTransform != null)
+ if (RenderTransform is IMutableTransform mutableTransform)
{
- RenderTransform.Changed += RenderTransformChanged;
+ mutableTransform.Changed += RenderTransformChanged;
}
EnableTransitions();
@@ -424,13 +424,13 @@ namespace Avalonia
/// The event args.
protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
- Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Visual, this, "Detached from visual tree");
+ Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Detached from visual tree");
_visualRoot = null;
- if (RenderTransform != null)
+ if (RenderTransform is IMutableTransform mutableTransform)
{
- RenderTransform.Changed -= RenderTransformChanged;
+ mutableTransform.Changed -= RenderTransformChanged;
}
DisableTransitions();
@@ -501,8 +501,7 @@ namespace Avalonia
return;
}
- Logger.TryGet(LogEventLevel.Warning)?.Log(
- LogArea.Binding,
+ Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log(
this,
"Error in binding to {Target}.{Property}: {Message}",
this,
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.Visuals/VisualTree/IVisualWithRoundRectClip.cs b/src/Avalonia.Visuals/VisualTree/IVisualWithRoundRectClip.cs
new file mode 100644
index 0000000000..9ace215d03
--- /dev/null
+++ b/src/Avalonia.Visuals/VisualTree/IVisualWithRoundRectClip.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace Avalonia.VisualTree
+{
+ [Obsolete("Internal API, will be removed in future versions, you've been warned")]
+ public interface IVisualWithRoundRectClip
+ {
+ ///
+ /// Gets a value indicating the corner radius of control's clip bounds
+ ///
+ [Obsolete("Internal API, will be removed in future versions, you've been warned")]
+ CornerRadius ClipToBoundsRadius { get; }
+
+ }
+}
diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs
index 046036fd68..e3250e6733 100644
--- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs
+++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs
@@ -37,7 +37,7 @@ namespace Avalonia.X11.Glx
}
catch(Exception e)
{
- Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", null, "Unable to initialize GLX-based rendering: {0}", e);
+ Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize GLX-based rendering: {0}", e);
return null;
}
}
diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs
index dd16fae386..32f1822e51 100644
--- a/src/Avalonia.X11/X11Atoms.cs
+++ b/src/Avalonia.X11/X11Atoms.cs
@@ -22,6 +22,7 @@
//
using System;
+using System.Collections.Generic;
using System.Linq;
using static Avalonia.X11.XLib;
// ReSharper disable FieldCanBeMadeReadOnly.Global
@@ -40,8 +41,9 @@ namespace Avalonia.X11
internal class X11Atoms
{
+ private readonly IntPtr _display;
-// Our atoms
+ // Our atoms
public readonly IntPtr AnyPropertyType = (IntPtr)0;
public readonly IntPtr XA_PRIMARY = (IntPtr)1;
public readonly IntPtr XA_SECONDARY = (IntPtr)2;
@@ -187,10 +189,13 @@ namespace Avalonia.X11
public readonly IntPtr ATOM_PAIR;
public readonly IntPtr MANAGER;
public readonly IntPtr _KDE_NET_WM_BLUR_BEHIND_REGION;
+ public readonly IntPtr INCR;
-
+ private readonly Dictionary _namesToAtoms = new Dictionary();
+ private readonly Dictionary _atomsToNames = new Dictionary();
public X11Atoms(IntPtr display)
{
+ _display = display;
// make sure this array stays in sync with the statements below
@@ -204,7 +209,33 @@ namespace Avalonia.X11
XInternAtoms(display, atomNames, atomNames.Length, true, atoms);
for (var c = 0; c < fields.Length; c++)
+ {
+ _namesToAtoms[fields[c].Name] = atoms[c];
+ _atomsToNames[atoms[c]] = fields[c].Name;
fields[c].SetValue(this, atoms[c]);
+ }
+ }
+
+ public IntPtr GetAtom(string name)
+ {
+ if (_namesToAtoms.TryGetValue(name, out var rv))
+ return rv;
+ var atom = XInternAtom(_display, name, false);
+ _namesToAtoms[name] = atom;
+ _atomsToNames[atom] = name;
+ return atom;
+ }
+
+ public string GetAtomName(IntPtr atom)
+ {
+ if (_atomsToNames.TryGetValue(atom, out var rv))
+ return rv;
+ var name = XLib.GetAtomName(_display, atom);
+ if (name == null)
+ return null;
+ _atomsToNames[atom] = name;
+ _namesToAtoms[name] = atom;
+ return name;
}
}
}
diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs
index a431ffcc1a..7023bb3ef3 100644
--- a/src/Avalonia.X11/X11Clipboard.cs
+++ b/src/Avalonia.X11/X11Clipboard.cs
@@ -1,8 +1,10 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
+using Avalonia.Input;
using Avalonia.Input.Platform;
using static Avalonia.X11.XLib;
namespace Avalonia.X11
@@ -10,12 +12,14 @@ namespace Avalonia.X11
class X11Clipboard : IClipboard
{
private readonly X11Info _x11;
- private string _storedString;
+ private IDataObject _storedDataObject;
private IntPtr _handle;
private TaskCompletionSource _requestedFormatsTcs;
- private TaskCompletionSource _requestedTextTcs;
+ private TaskCompletionSource _requestedDataTcs;
private readonly IntPtr[] _textAtoms;
private readonly IntPtr _avaloniaSaveTargetsAtom;
+ private readonly Dictionary _formatAtoms = new Dictionary();
+ private readonly Dictionary _atomFormats = new Dictionary();
public X11Clipboard(AvaloniaX11Platform platform)
{
@@ -31,6 +35,11 @@ namespace Avalonia.X11
}.Where(a => a != IntPtr.Zero).ToArray();
}
+ bool IsStringAtom(IntPtr atom)
+ {
+ return _textAtoms.Contains(atom);
+ }
+
Encoding GetStringEncoding(IntPtr atom)
{
return (atom == _x11.Atoms.XA_STRING
@@ -75,21 +84,31 @@ namespace Avalonia.X11
Encoding textEnc;
if (target == _x11.Atoms.TARGETS)
{
- var atoms = _textAtoms;
- atoms = atoms.Concat(new[] {_x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE})
- .ToArray();
+ var atoms = new HashSet { _x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE };
+ foreach (var fmt in _storedDataObject.GetDataFormats())
+ {
+ if (fmt == DataFormats.Text)
+ foreach (var ta in _textAtoms)
+ atoms.Add(ta);
+ else
+ atoms.Add(_x11.Atoms.GetAtom(fmt));
+ }
+
XChangeProperty(_x11.Display, window, property,
- _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length);
+ _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms.ToArray(), atoms.Count);
return property;
}
else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero)
{
return property;
}
- else if ((textEnc = GetStringEncoding(target)) != null)
+ else if ((textEnc = GetStringEncoding(target)) != null
+ && _storedDataObject?.Contains(DataFormats.Text) == true)
{
-
- var data = textEnc.GetBytes(_storedString ?? "");
+ var text = _storedDataObject.GetText();
+ if(text == null)
+ return IntPtr.Zero;
+ var data = textEnc.GetBytes(text);
fixed (void* pdata = data)
XChangeProperty(_x11.Display, window, property, target, 8,
PropertyMode.Replace,
@@ -121,6 +140,23 @@ namespace Avalonia.X11
return property;
}
+ else if(_storedDataObject?.Contains(_x11.Atoms.GetAtomName(target)) == true)
+ {
+ var objValue = _storedDataObject.Get(_x11.Atoms.GetAtomName(target));
+
+ if(!(objValue is byte[] bytes))
+ {
+ if (objValue is string s)
+ bytes = Encoding.UTF8.GetBytes(s);
+ else
+ return IntPtr.Zero;
+ }
+
+ XChangeProperty(_x11.Display, window, property, target, 8,
+ PropertyMode.Replace,
+ bytes, bytes.Length);
+ return property;
+ }
else
return IntPtr.Zero;
}
@@ -131,15 +167,15 @@ namespace Avalonia.X11
if (sel.property == IntPtr.Zero)
{
_requestedFormatsTcs?.TrySetResult(null);
- _requestedTextTcs?.TrySetResult(null);
+ _requestedDataTcs?.TrySetResult(null);
}
XGetWindowProperty(_x11.Display, _handle, sel.property, IntPtr.Zero, new IntPtr (0x7fffffff), true, (IntPtr)Atom.AnyPropertyType,
- out var actualAtom, out var actualFormat, out var nitems, out var bytes_after, out var prop);
+ out var actualTypeAtom, out var actualFormat, out var nitems, out var bytes_after, out var prop);
Encoding textEnc = null;
if (nitems == IntPtr.Zero)
{
_requestedFormatsTcs?.TrySetResult(null);
- _requestedTextTcs?.TrySetResult(null);
+ _requestedDataTcs?.TrySetResult(null);
}
else
{
@@ -154,10 +190,24 @@ namespace Avalonia.X11
_requestedFormatsTcs?.TrySetResult(formats);
}
}
- else if ((textEnc = GetStringEncoding(sel.property)) != null)
+ else if ((textEnc = GetStringEncoding(actualTypeAtom)) != null)
{
var text = textEnc.GetString((byte*)prop.ToPointer(), nitems.ToInt32());
- _requestedTextTcs?.TrySetResult(text);
+ _requestedDataTcs?.TrySetResult(text);
+ }
+ else
+ {
+ if (actualTypeAtom == _x11.Atoms.INCR)
+ {
+ // TODO: Actually implement that monstrosity
+ _requestedDataTcs.TrySetResult(null);
+ }
+ else
+ {
+ var data = new byte[(int)nitems * (actualFormat / 8)];
+ Marshal.Copy(prop, data, 0, data.Length);
+ _requestedDataTcs?.TrySetResult(data);
+ }
}
}
@@ -174,17 +224,19 @@ namespace Avalonia.X11
return _requestedFormatsTcs.Task;
}
- Task SendTextRequest(IntPtr format)
+ Task SendDataRequest(IntPtr format)
{
- if (_requestedTextTcs == null || _requestedFormatsTcs.Task.IsCompleted)
- _requestedTextTcs = new TaskCompletionSource();
+ if (_requestedDataTcs == null || _requestedFormatsTcs.Task.IsCompleted)
+ _requestedDataTcs = new TaskCompletionSource();
XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD, format, format, _handle, IntPtr.Zero);
- return _requestedTextTcs.Task;
+ return _requestedDataTcs.Task;
}
+
+ bool HasOwner => XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) != IntPtr.Zero;
public async Task GetTextAsync()
{
- if (XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) == IntPtr.Zero)
+ if (!HasOwner)
return null;
var res = await SendFormatRequest();
var target = _x11.Atoms.UTF8_STRING;
@@ -199,7 +251,7 @@ namespace Avalonia.X11
}
}
- return await SendTextRequest(target);
+ return (string)await SendDataRequest(target);
}
void StoreAtomsInClipboardManager(IntPtr[] atoms)
@@ -220,15 +272,52 @@ namespace Avalonia.X11
public Task SetTextAsync(string text)
{
- _storedString = text;
+ var data = new DataObject();
+ data.Set(DataFormats.Text, text);
+ return SetDataObjectAsync(data);
+ }
+
+ public Task ClearAsync()
+ {
+ return SetTextAsync(null);
+ }
+
+ public Task SetDataObjectAsync(IDataObject data)
+ {
+ _storedDataObject = data;
XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero);
StoreAtomsInClipboardManager(_textAtoms);
return Task.CompletedTask;
}
- public Task ClearAsync()
+ public async Task GetFormatsAsync()
{
- return SetTextAsync(null);
+ if (!HasOwner)
+ return null;
+ var res = await SendFormatRequest();
+ if (res == null)
+ return null;
+ var rv = new List();
+ if (_textAtoms.Any(res.Contains))
+ rv.Add(DataFormats.Text);
+ foreach (var t in res)
+ rv.Add(_x11.Atoms.GetAtomName(t));
+ return rv.ToArray();
+ }
+
+ public async Task GetDataAsync(string format)
+ {
+ if (!HasOwner)
+ return null;
+ if (format == DataFormats.Text)
+ return await GetTextAsync();
+
+ var formatAtom = _x11.Atoms.GetAtom(format);
+ var res = await SendFormatRequest();
+ if (!res.Contains(formatAtom))
+ return null;
+
+ return await SendDataRequest(formatAtom);
}
}
}
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/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs
index 3c41f7bdde..85aa4862b7 100644
--- a/src/Avalonia.X11/XLib.cs
+++ b/src/Avalonia.X11/XLib.cs
@@ -236,6 +236,10 @@ namespace Avalonia.X11
public static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type,
int format, PropertyMode mode, ref IntPtr value, int nelements);
+ [DllImport(libX11)]
+ public static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type,
+ int format, PropertyMode mode, byte[] data, int nelements);
+
[DllImport(libX11)]
public static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type,
int format, PropertyMode mode, uint[] data, int nelements);
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
index cd67b27ff3..45ca1c4adc 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
@@ -39,8 +39,7 @@ namespace Avalonia.Markup.Xaml.Converters
!property.IsAttached &&
!registry.IsRegistered(targetType, property))
{
- Logger.TryGet(LogEventLevel.Warning)?.Log(
- LogArea.Property,
+ Logger.TryGet(LogEventLevel.Warning, LogArea.Property)?.Log(
this,
"Property '{Owner}.{Name}' is not registered on '{Type}'.",
effectiveOwner,
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/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs b/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs
index ebb544bcca..0b0ed7b06a 100644
--- a/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Data/DelayedBinding.cs
@@ -147,8 +147,7 @@ namespace Avalonia.Markup.Data
}
catch (Exception e)
{
- Logger.TryGet(LogEventLevel.Error)?.Log(
- LogArea.Property,
+ Logger.TryGet(LogEventLevel.Error, LogArea.Property)?.Log(
control,
"Error setting {Property} on {Target}: {Exception}",
Property.Name,
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index 26fdb08a4b..ae756f4eab 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -389,6 +389,12 @@ namespace Avalonia.Skia
Canvas.ClipRect(clip.ToSKRect());
}
+ public void PushClip(RoundedRect clip)
+ {
+ Canvas.Save();
+ Canvas.ClipRoundRect(clip.ToSKRoundRect());
+ }
+
///
public void PopClip()
{
diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
index 6375f74c59..ec7e0a67ed 100644
--- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
+++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
@@ -40,6 +40,21 @@ namespace Avalonia.Skia
return new SKRect((float)r.X, (float)r.Y, (float)r.Right, (float)r.Bottom);
}
+ public static SKRoundRect ToSKRoundRect(this RoundedRect r)
+ {
+ var rc = r.Rect.ToSKRect();
+ var result = new SKRoundRect();
+
+ result.SetRectRadii(rc,
+ new[]
+ {
+ r.RadiiTopLeft.ToSKPoint(), r.RadiiTopRight.ToSKPoint(),
+ r.RadiiBottomRight.ToSKPoint(), r.RadiiBottomLeft.ToSKPoint(),
+ });
+
+ return result;
+ }
+
public static Rect ToAvaloniaRect(this SKRect r)
{
return new Rect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
index bbb45cf64c..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)
{
@@ -359,6 +359,12 @@ namespace Avalonia.Direct2D1.Media
_deviceContext.PushAxisAlignedClip(clip.ToSharpDX(), AntialiasMode.PerPrimitive);
}
+ public void PushClip(RoundedRect clip)
+ {
+ //TODO: radius
+ _deviceContext.PushAxisAlignedClip(clip.Rect.ToDirect2D(), AntialiasMode.PerPrimitive);
+ }
+
public void PopClip()
{
_deviceContext.PopAxisAlignedClip();
diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs
index cca014bda5..e1f7aad1b2 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs
@@ -82,8 +82,7 @@ namespace Avalonia.Direct2D1.Media
}
catch (Exception ex)
{
- Logger.TryGet(LogEventLevel.Error)?.Log(
- LogArea.Visual,
+ Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(
this,
"GeometrySink.Close exception: {Exception}",
ex);
diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs
index 1c94d7a446..7d9e0a8bd2 100644
--- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs
+++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs
@@ -1,25 +1,30 @@
using System;
+using System.Linq;
+using System.Reactive.Disposables;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
+using Avalonia.Input;
using Avalonia.Input.Platform;
+using Avalonia.Threading;
using Avalonia.Win32.Interop;
namespace Avalonia.Win32
{
internal class ClipboardImpl : IClipboard
{
- private async Task OpenClipboard()
+ private async Task OpenClipboard()
{
while (!UnmanagedMethods.OpenClipboard(IntPtr.Zero))
{
await Task.Delay(100);
}
+
+ return Disposable.Create(() => UnmanagedMethods.CloseClipboard());
}
public async Task GetTextAsync()
{
- await OpenClipboard();
- try
+ using(await OpenClipboard())
{
IntPtr hText = UnmanagedMethods.GetClipboardData(UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT);
if (hText == IntPtr.Zero)
@@ -37,10 +42,6 @@ namespace Avalonia.Win32
UnmanagedMethods.GlobalUnlock(hText);
return rv;
}
- finally
- {
- UnmanagedMethods.CloseClipboard();
- }
}
public async Task SetTextAsync(string text)
@@ -50,31 +51,66 @@ namespace Avalonia.Win32
throw new ArgumentNullException(nameof(text));
}
- await OpenClipboard();
-
- UnmanagedMethods.EmptyClipboard();
-
- try
+ using(await OpenClipboard())
{
+ UnmanagedMethods.EmptyClipboard();
+
var hGlobal = Marshal.StringToHGlobalUni(text);
UnmanagedMethods.SetClipboardData(UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, hGlobal);
}
- finally
- {
- UnmanagedMethods.CloseClipboard();
- }
}
public async Task ClearAsync()
{
- await OpenClipboard();
- try
+ using(await OpenClipboard())
{
UnmanagedMethods.EmptyClipboard();
}
- finally
+ }
+
+ public async Task SetDataObjectAsync(IDataObject data)
+ {
+ Dispatcher.UIThread.VerifyAccess();
+ var wrapper = new DataObject(data);
+ while (true)
{
- UnmanagedMethods.CloseClipboard();
+ if (UnmanagedMethods.OleSetClipboard(wrapper) == 0)
+ break;
+ await Task.Delay(100);
+ }
+ }
+
+ public async Task GetFormatsAsync()
+ {
+ Dispatcher.UIThread.VerifyAccess();
+ while (true)
+ {
+ if (UnmanagedMethods.OleGetClipboard(out var dataObject) == 0)
+ {
+ var wrapper = new OleDataObject(dataObject);
+ var formats = wrapper.GetDataFormats().ToArray();
+ Marshal.ReleaseComObject(dataObject);
+ return formats;
+ }
+
+ await Task.Delay(100);
+ }
+ }
+
+ public async Task GetDataAsync(string format)
+ {
+ Dispatcher.UIThread.VerifyAccess();
+ while (true)
+ {
+ if (UnmanagedMethods.OleGetClipboard(out var dataObject) == 0)
+ {
+ var wrapper = new OleDataObject(dataObject);
+ var rv = wrapper.Get(format);
+ Marshal.ReleaseComObject(dataObject);
+ return rv;
+ }
+
+ await Task.Delay(100);
}
}
}
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index da52c3116e..ba3775200b 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -1169,6 +1169,12 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
public static extern IntPtr SetClipboardData(ClipboardFormat uFormat, IntPtr hMem);
+ [DllImport("ole32.dll", PreserveSig = false)]
+ public static extern int OleGetClipboard(out IOleDataObject dataObject);
+
+ [DllImport("ole32.dll", PreserveSig = true)]
+ public static extern int OleSetClipboard(IOleDataObject dataObject);
+
[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern IntPtr GlobalLock(IntPtr handle);
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/src/iOS/Avalonia.iOS/Clipboard.cs b/src/iOS/Avalonia.iOS/Clipboard.cs
index c66b379453..2deb49473f 100644
--- a/src/iOS/Avalonia.iOS/Clipboard.cs
+++ b/src/iOS/Avalonia.iOS/Clipboard.cs
@@ -22,5 +22,11 @@ namespace Avalonia.iOS
UIPasteboard.General.String = "";
return Task.FromResult(0);
}
+
+ public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException();
+
+ public Task GetFormatsAsync() => throw new PlatformNotSupportedException();
+
+ public Task GetDataAsync(string format) => throw new PlatformNotSupportedException();
}
}
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 5c2502422a..6c5e0d3b53 100644
--- a/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs
+++ b/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs
@@ -1,17 +1,15 @@
using System;
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
using Avalonia.Data.Core.Plugins;
using BenchmarkDotNet.Attributes;
-using JetBrains.Annotations;
namespace Avalonia.Benchmarks.Data
{
[MemoryDiagnoser, InProcess]
public class PropertyAccessorBenchmarks
{
- private readonly InpcPropertyAccessorPlugin _plugin = new InpcPropertyAccessorPlugin();
- private readonly TestObject _targetStrongRef = new TestObject();
+ private readonly InpcPropertyAccessorPlugin _inpcPlugin = new InpcPropertyAccessorPlugin();
+ private readonly MethodAccessorPlugin _methodPlugin = new MethodAccessorPlugin();
+ private readonly AccessorTestObject _targetStrongRef = new AccessorTestObject();
private readonly WeakReference _targetWeakRef;
public PropertyAccessorBenchmarks()
@@ -20,38 +18,27 @@ namespace Avalonia.Benchmarks.Data
}
[Benchmark]
- public void InpcAccessor()
+ public void InpcAccessorMatch()
{
- _plugin.Start(_targetWeakRef, nameof(TestObject.Test));
+ _inpcPlugin.Match(_targetWeakRef, nameof(AccessorTestObject.Test));
}
- private class TestObject : INotifyPropertyChanged
+ [Benchmark]
+ public void InpcAccessorStart()
{
- private string _test;
-
- public string Test
- {
- get => _test;
- set
- {
- if (_test == value)
- {
- return;
- }
-
- _test = value;
-
- OnPropertyChanged();
- }
- }
+ _inpcPlugin.Start(_targetWeakRef, nameof(AccessorTestObject.Test));
+ }
- public event PropertyChangedEventHandler PropertyChanged;
+ [Benchmark]
+ public void MethodAccessorMatch()
+ {
+ _methodPlugin.Match(_targetWeakRef, nameof(AccessorTestObject.Execute));
+ }
- [NotifyPropertyChangedInvocator]
- protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
+ [Benchmark]
+ public void MethodAccessorStart()
+ {
+ _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/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index d2f62cde04..217dec2d6d 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
@@ -654,6 +654,11 @@ namespace Avalonia.Controls.UnitTests
public Task SetTextAsync(string text) => Task.CompletedTask;
public Task ClearAsync() => Task.CompletedTask;
+ public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
+
+ public Task GetFormatsAsync() => Task.FromResult(Array.Empty());
+
+ public Task GetDataAsync(string format) => Task.FromResult((object)null);
}
}
}
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.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj
index 73a1d586b5..523c5e7ff4 100644
--- a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj
+++ b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj
@@ -32,5 +32,4 @@
-
diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs
index 7013e93a43..a21c8d589d 100644
--- a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs
+++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs
@@ -213,9 +213,7 @@ namespace Avalonia.Layout.UnitTests
{
Child = border1 = new Border
{
- Child = border2 = new Border
- {
- }
+ Child = border2 = new Border(),
},
LayoutManager = layoutManager,
};
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.UnitTests/TestLogSink.cs b/tests/Avalonia.UnitTests/TestLogSink.cs
index 5c1dd293c4..e10292a59b 100644
--- a/tests/Avalonia.UnitTests/TestLogSink.cs
+++ b/tests/Avalonia.UnitTests/TestLogSink.cs
@@ -27,7 +27,7 @@ namespace Avalonia.UnitTests
return Disposable.Create(() => Logger.Sink = null);
}
- public bool IsEnabled(LogEventLevel level)
+ public bool IsEnabled(LogEventLevel level, string area)
{
return true;
}
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);
+ }
+ }
+}