diff --git a/.ncrunch/Avalonia.Web.Blazor.v3.ncrunchproject b/.ncrunch/Avalonia.Web.Blazor.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/Avalonia.Web.Blazor.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/.ncrunch/Avalonia.Web.v3.ncrunchproject b/.ncrunch/Avalonia.Web.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/Avalonia.Web.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/.ncrunch/Avalonia.Win32.net6.0.v3.ncrunchproject b/.ncrunch/Avalonia.Win32.net6.0.v3.ncrunchproject
index 28b692bb51..95a483b433 100644
--- a/.ncrunch/Avalonia.Win32.net6.0.v3.ncrunchproject
+++ b/.ncrunch/Avalonia.Win32.net6.0.v3.ncrunchproject
@@ -1,7 +1,3 @@
-
-
- ..\..\tools\MicroComGenerator\bin\Debug\net6.0\**.*
-
-
+
\ No newline at end of file
diff --git a/.ncrunch/Avalonia.Win32.netstandard2.0.v3.ncrunchproject b/.ncrunch/Avalonia.Win32.netstandard2.0.v3.ncrunchproject
index 28b692bb51..95a483b433 100644
--- a/.ncrunch/Avalonia.Win32.netstandard2.0.v3.ncrunchproject
+++ b/.ncrunch/Avalonia.Win32.netstandard2.0.v3.ncrunchproject
@@ -1,7 +1,3 @@
-
-
- ..\..\tools\MicroComGenerator\bin\Debug\net6.0\**.*
-
-
+
\ No newline at end of file
diff --git a/.ncrunch/ControlCatalog.Blazor.Web.v3.ncrunchproject b/.ncrunch/ControlCatalog.Blazor.Web.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/ControlCatalog.Blazor.Web.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/.ncrunch/MobileSandbox.Android.v3.ncrunchproject b/.ncrunch/MobileSandbox.Android.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/MobileSandbox.Android.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/.ncrunch/MobileSandbox.Desktop.v3.ncrunchproject b/.ncrunch/MobileSandbox.Desktop.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/MobileSandbox.Desktop.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/.ncrunch/MobileSandbox.iOS.v3.ncrunchproject b/.ncrunch/MobileSandbox.iOS.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/MobileSandbox.iOS.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/.ncrunch/MobileSandbox.net6.0.v3.ncrunchproject b/.ncrunch/MobileSandbox.net6.0.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/MobileSandbox.net6.0.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/.ncrunch/MobileSandbox.netstandard2.0.v3.ncrunchproject b/.ncrunch/MobileSandbox.netstandard2.0.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/MobileSandbox.netstandard2.0.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/.ncrunch/_build.v3.ncrunchproject b/.ncrunch/_build.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/_build.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf
new file mode 100644
index 0000000000..6ba05332be
--- /dev/null
+++ b/Avalonia.Desktop.slnf
@@ -0,0 +1,60 @@
+{
+ "solution": {
+ "path": "Avalonia.sln",
+ "projects": [
+ "packages\\Avalonia\\Avalonia.csproj",
+ "samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj",
+ "samples\\ControlCatalog\\ControlCatalog.csproj",
+ "samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
+ "samples\\MiniMvvm\\MiniMvvm.csproj",
+ "samples\\SampleControls\\ControlSamples.csproj",
+ "samples\\Sandbox\\Sandbox.csproj",
+ "src\\Avalonia.Base\\Avalonia.Base.csproj",
+ "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
+ "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
+ "src\\Avalonia.Controls.DataGrid\\Avalonia.Controls.DataGrid.csproj",
+ "src\\Avalonia.Controls\\Avalonia.Controls.csproj",
+ "src\\Avalonia.DesignerSupport\\Avalonia.DesignerSupport.csproj",
+ "src\\Avalonia.Desktop\\Avalonia.Desktop.csproj",
+ "src\\Avalonia.Diagnostics\\Avalonia.Diagnostics.csproj",
+ "src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj",
+ "src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj",
+ "src\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj",
+ "src\\Avalonia.Headless\\Avalonia.Headless.csproj",
+ "src\\Avalonia.MicroCom\\Avalonia.MicroCom.csproj",
+ "src\\Avalonia.Native\\Avalonia.Native.csproj",
+ "src\\Avalonia.OpenGL\\Avalonia.OpenGL.csproj",
+ "src\\Avalonia.ReactiveUI\\Avalonia.ReactiveUI.csproj",
+ "src\\Avalonia.Remote.Protocol\\Avalonia.Remote.Protocol.csproj",
+ "src\\Avalonia.Themes.Fluent\\Avalonia.Themes.Fluent.csproj",
+ "src\\Avalonia.Themes.Simple\\Avalonia.Themes.Simple.csproj",
+ "src\\Avalonia.X11\\Avalonia.X11.csproj",
+ "src\\Linux\\Avalonia.LinuxFramebuffer\\Avalonia.LinuxFramebuffer.csproj",
+ "src\\Markup\\Avalonia.Markup.Xaml.Loader\\Avalonia.Markup.Xaml.Loader.csproj",
+ "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj",
+ "src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj",
+ "src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj",
+ "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
+ "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
+ "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
+ "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
+ "src\\tools\\DevGenerators\\DevGenerators.csproj",
+ "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
+ "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
+ "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",
+ "tests\\Avalonia.Controls.UnitTests\\Avalonia.Controls.UnitTests.csproj",
+ "tests\\Avalonia.DesignerSupport.TestApp\\Avalonia.DesignerSupport.TestApp.csproj",
+ "tests\\Avalonia.DesignerSupport.Tests\\Avalonia.DesignerSupport.Tests.csproj",
+ "tests\\Avalonia.Direct2D1.RenderTests\\Avalonia.Direct2D1.RenderTests.csproj",
+ "tests\\Avalonia.Direct2D1.UnitTests\\Avalonia.Direct2D1.UnitTests.csproj",
+ "tests\\Avalonia.IntegrationTests.Appium\\Avalonia.IntegrationTests.Appium.csproj",
+ "tests\\Avalonia.LeakTests\\Avalonia.LeakTests.csproj",
+ "tests\\Avalonia.Markup.UnitTests\\Avalonia.Markup.UnitTests.csproj",
+ "tests\\Avalonia.Markup.Xaml.UnitTests\\Avalonia.Markup.Xaml.UnitTests.csproj",
+ "tests\\Avalonia.ReactiveUI.UnitTests\\Avalonia.ReactiveUI.UnitTests.csproj",
+ "tests\\Avalonia.Skia.RenderTests\\Avalonia.Skia.RenderTests.csproj",
+ "tests\\Avalonia.Skia.UnitTests\\Avalonia.Skia.UnitTests.csproj",
+ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/Avalonia.sln b/Avalonia.sln
index 461de8530b..e2f04ddc35 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -522,6 +522,7 @@ Global
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Release|Any CPU.Build.0 = Release|Any CPU
{C90FE60B-B01E-4F35-91D6-379D6966030F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C90FE60B-B01E-4F35-91D6-379D6966030F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C90FE60B-B01E-4F35-91D6-379D6966030F}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{C90FE60B-B01E-4F35-91D6-379D6966030F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C90FE60B-B01E-4F35-91D6-379D6966030F}.Release|Any CPU.Build.0 = Release|Any CPU
{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000000..73954c7f4d
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,5 @@
+
+
+ $(DefineConstants);NET7SDK
+
+
diff --git a/Documentation/build.md b/Documentation/build.md
index ddd38be887..fd6b26337c 100644
--- a/Documentation/build.md
+++ b/Documentation/build.md
@@ -1,8 +1,8 @@
# Windows
-Avalonia requires at least Visual Studio 2022 and dotnet 6 SDK 6.0.100 to build on all platforms.
+Avalonia requires at least Visual Studio 2022 and dotnet 7-rc2 SDK 7.0.100-rc.2 to build on all platforms.
-### Clone the Avalonia repository
+## Clone the Avalonia repository
```
git clone https://github.com/AvaloniaUI/Avalonia.git
@@ -10,15 +10,30 @@ cd Avalonia
git submodule update --init
```
-### Install the required version of the .NET Core SDK
+## Install the required version of the .NET Core SDK
Go to https://dotnet.microsoft.com/download/visual-studio-sdks and install the latest version of the .NET Core SDK compatible with Avalonia UI. Make sure to download the SDK (not just the "runtime") package. The version compatible is indicated within the [global.json](https://github.com/AvaloniaUI/Avalonia/blob/master/global.json) file. Note that Avalonia UI does not always use the latest version and is hardcoded to use the last version known to be compatible (SDK releases may break the builds from time-to-time).
-### Open in Visual Studio
+## Build and Run Avalonia
-Open the `Avalonia.sln` solution in Visual Studio 2022 or newer. The free Visual Studio Community edition works fine. Build and run the `Samples\ControlCatalog.Desktop` or `ControlCatalog.NetCore` project to see the sample application.
+```
+cd samples\ControlCatalog.NetCore
+dotnet restore
+dotnet run
+```
+
+## Opening in Visual Studio
-### Troubleshooting
+If you want to open Avalonia in Visual Studio you have two options:
+
+- Avalonia.sln: This contains the whole of Avalonia in including desktop, mobile and web. You must have a number of dotnet workloads installed in order to build everything in this solution
+- Avalonia.Desktop.slnf: This solution filter opens only the parts of Avalonia required to run on desktop. This requires no extra workloads to be installed.
+
+Avalonia requires Visual Studio 2022 or newer. The free Visual Studio Community edition works fine.
+
+Build and run `ControlCatalog.NetCore` project to see the sample application.
+
+### Visual Studio Troubleshooting
* **Error CS0006: Avalonia.DesktopRuntime.dll could not be found**
@@ -35,16 +50,15 @@ It's *not* possible to build the *whole* project on Linux/macOS. You can only bu
MonoDevelop, Xamarin Studio and Visual Studio for Mac aren't capable of properly opening our solution. You can use Rider (at least 2017.2 EAP) or VSCode instead. They will fail to load most of platform specific projects, but you don't need them to run on .NET Core.
-### Install the latest version of the .NET Core SDK
+## Install the latest version of the .NET Core SDK
Go to https://www.microsoft.com/net/core and follow the instructions for your OS. Make sure to download the SDK (not just the "runtime") package.
-### Additional requirements for macOS
+## Additional requirements for macOS
The build process needs [Xcode](https://developer.apple.com/xcode/) to build the native library. Following the install instructions at the [Xcode](https://developer.apple.com/xcode/) website to properly install.
-
-### Clone the Avalonia repository
+## Clone the Avalonia repository
```
git clone https://github.com/AvaloniaUI/Avalonia.git
@@ -52,7 +66,7 @@ cd Avalonia
git submodule update --init --recursive
```
-### Build native libraries (macOS only)
+## Build native libraries (macOS only)
On macOS it is necessary to build and manually install the respective native libraries using [Xcode](https://developer.apple.com/xcode/). Execute the build script in the root project with the `CompileNative` task. It will build the headers, build the libraries, and place them in the appropriate place to allow .NET to find them at compilation and run time.
@@ -60,7 +74,7 @@ On macOS it is necessary to build and manually install the respective native lib
./build.sh CompileNative
```
-### Build and Run Avalonia
+## Build and Run Avalonia
```
cd samples/ControlCatalog.NetCore
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 903f9e3843..a3bbc33418 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -35,9 +35,9 @@ jobs:
version: 6.0.401
- task: UseDotNet@2
- displayName: 'Use .NET Core SDK 7.0.100-rc.2.22477.23'
+ displayName: 'Use .NET Core SDK 7.0'
inputs:
- version: 7.0.100-rc.2.22477.23
+ version: 7.0.100
- task: CmdLine@2
displayName: 'Install Workloads'
@@ -72,9 +72,9 @@ jobs:
version: 6.0.401
- task: UseDotNet@2
- displayName: 'Use .NET Core SDK 7.0.100-rc.2.22477.23'
+ displayName: 'Use .NET Core SDK 7.0.100'
inputs:
- version: 7.0.100-rc.2.22477.23
+ version: 7.0.100
- task: CmdLine@2
displayName: 'Install Workloads'
@@ -143,9 +143,9 @@ jobs:
version: 6.0.401
- task: UseDotNet@2
- displayName: 'Use .NET Core SDK 7.0.100-rc.2.22477.23'
+ displayName: 'Use .NET Core SDK 7.0.100'
inputs:
- version: 7.0.100-rc.2.22477.23
+ version: 7.0.100
- task: CmdLine@2
displayName: 'Install Workloads'
diff --git a/global.json b/global.json
index dc6da556b3..a9318b212f 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "7.0.100-rc.2.22477.23",
+ "version": "7.0.100",
"rollForward": "latestFeature"
},
"msbuild-sdks": {
diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm
index ebd9f39d30..4c7341f834 100644
--- a/native/Avalonia.Native/src/OSX/AvnWindow.mm
+++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm
@@ -385,7 +385,7 @@
return true;
}
--(void)resignKeyWindow
+-(void)windowDidResignKey:(NSNotification *)notification
{
if(_parent)
_parent->BaseEvents->Deactivated();
@@ -393,8 +393,6 @@
[self showAppMenuOnly];
[self invalidateShadow];
-
- [super resignKeyWindow];
}
- (void)windowDidMove:(NSNotification *_Nonnull)notification
diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm
index ddc50c26b6..2443965957 100644
--- a/native/Avalonia.Native/src/OSX/WindowImpl.mm
+++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm
@@ -63,7 +63,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
START_COM_CALL;
@autoreleasepool {
- _isDialog = isDialog;
+ _isDialog = isDialog || _parent != nullptr;
WindowBaseImpl::Show(activate, isDialog);
@@ -96,6 +96,8 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) {
auto cparent = dynamic_cast(parent);
_parent = cparent;
+
+ _isDialog = _parent != nullptr;
if(_parent != nullptr && Window != nullptr){
// If one tries to show a child window with a minimized parent window, then the parent window will be
diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm
index a15d0c9601..9cc9fc9523 100644
--- a/native/Avalonia.Native/src/OSX/app.mm
+++ b/native/Avalonia.Native/src/OSX/app.mm
@@ -95,11 +95,14 @@ ComPtr _events;
}
@end
-extern void InitializeAvnApp(IAvnApplicationEvents* events)
+extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate)
{
- NSApplication* app = [AvnApplication sharedApplication];
- id delegate = [[AvnAppDelegate alloc] initWithEvents:events];
- [app setDelegate:delegate];
+ if(!disableAppDelegate)
+ {
+ NSApplication* app = [AvnApplication sharedApplication];
+ id delegate = [[AvnAppDelegate alloc] initWithEvents:events];
+ [app setDelegate:delegate];
+ }
}
HRESULT AvnApplicationCommands::HideApp()
diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h
index a90a235b9d..7ee7205776 100644
--- a/native/Avalonia.Native/src/OSX/common.h
+++ b/native/Avalonia.Native/src/OSX/common.h
@@ -32,7 +32,7 @@ extern void SetServicesMenu (IAvnMenu* menu);
extern IAvnMenu* GetAppMenu ();
extern NSMenuItem* GetAppMenuItem ();
-extern void InitializeAvnApp(IAvnApplicationEvents* events);
+extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate);
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
extern NSPoint ToNSPoint (AvnPoint p);
extern NSRect ToNSRect (AvnRect r);
diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm
index 6ee86b21ae..c29d4108d0 100644
--- a/native/Avalonia.Native/src/OSX/main.mm
+++ b/native/Avalonia.Native/src/OSX/main.mm
@@ -3,6 +3,8 @@
#include "common.h"
static NSString* s_appTitle = @"Avalonia";
+static int disableSetProcessName = 0;
+static bool disableAppDelegate = false;
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
@@ -111,11 +113,17 @@ public:
@autoreleasepool
{
auto appTitle = [NSString stringWithUTF8String: utf8String];
-
- [[NSProcessInfo processInfo] setProcessName:appTitle];
-
-
- SetProcessName(appTitle);
+ if (disableSetProcessName == 0)
+ {
+ [[NSProcessInfo processInfo] setProcessName:appTitle];
+
+ SetProcessName(appTitle);
+ }
+ if (disableSetProcessName == 1)
+ {
+ auto rootMenu = [NSApp mainMenu];
+ [rootMenu setTitle:appTitle];
+ }
return S_OK;
}
@@ -133,6 +141,27 @@ public:
}
}
+ virtual HRESULT SetDisableSetProcessName(int disable) override
+ {
+ START_COM_CALL;
+
+ @autoreleasepool
+ {
+ disableSetProcessName = disable;
+ return S_OK;
+ }
+ }
+
+ virtual HRESULT SetDisableAppDelegate(int disable) override
+ {
+ START_COM_CALL;
+
+ @autoreleasepool {
+ disableAppDelegate = disable;
+ return S_OK;
+ }
+ }
+
};
/// See "Using POSIX Threads in a Cocoa Application" section here:
@@ -175,7 +204,7 @@ public:
@autoreleasepool{
[[ThreadingInitializer new] do];
}
- InitializeAvnApp(events);
+ InitializeAvnApp(events, disableAppDelegate);
return S_OK;
};
diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm
index 2075cc85ab..1c22c91207 100644
--- a/native/Avalonia.Native/src/OSX/rendertarget.mm
+++ b/native/Avalonia.Native/src/OSX/rendertarget.mm
@@ -183,8 +183,11 @@ static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* ta
[_layer setContents: (__bridge IOSurface*) surface->surface];
}
[CATransaction commit];
- [CATransaction flush];
}
+ // This can trigger event processing on the main thread
+ // which might need to lock the renderer
+ // which can cause a deadlock. So flush call is outside of the lock
+ [CATransaction flush];
}
else
dispatch_async(dispatch_get_main_queue(), ^{
diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs
index 7425c344c3..2295c0beda 100644
--- a/nukebuild/Build.cs
+++ b/nukebuild/Build.cs
@@ -145,8 +145,25 @@ partial class Build : NukeBuild
{
Information($"Running tests from {projectName}");
var project = Solution.GetProject(projectName).NotNull("project != null");
+ // Nuke and MSBuild tools have build-in helpers to get target frameworks from the project.
+ // Unfortunately, it gets broken with every second SDK update, so we had to do it manually.
+ var fileXml = XDocument.Parse(File.ReadAllText(project.Path));
+ var targetFrameworks = fileXml.Descendants("TargetFrameworks")
+ .FirstOrDefault()?.Value.Split(';').Select(f => f.Trim());
+ if (targetFrameworks is null)
+ {
+ var targetFramework = fileXml.Descendants("TargetFramework").FirstOrDefault()?.Value;
+ if (targetFramework is not null)
+ {
+ targetFrameworks = new[] { targetFramework };
+ }
+ }
+ if (targetFrameworks is null)
+ {
+ throw new InvalidOperationException("No target frameworks were found in the test project");
+ }
- foreach (var fw in project.GetTargetFrameworks())
+ foreach (var fw in targetFrameworks)
{
if (fw.StartsWith("net4")
&& RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj
index 8c0d824298..92d9732e91 100644
--- a/nukebuild/_build.csproj
+++ b/nukebuild/_build.csproj
@@ -13,11 +13,11 @@
-
+
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/readme.md b/readme.md
index 1009e86c29..c2be487af3 100644
--- a/readme.md
+++ b/readme.md
@@ -104,7 +104,7 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
## Commercial Support
-We have a range of [support plans available](https://avaloniaui.net/support.html) for those looking to partner with the creators of Avalonia, enabling access to the best support at every step of the development process.
+We have a range of [support plans available](https://avaloniaui.net/support) for those looking to partner with the creators of Avalonia, enabling access to the best support at every step of the development process.
*Please note that donations are not considered payment for commercial support agreements. Please contact us to discuss your needs first. [team@avaloniaui.net](mailto://team@avaloniaui.net)*
## .NET Foundation
diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml
index 32914428ed..6bb428e2c7 100644
--- a/samples/ControlCatalog/Pages/TextBlockPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml
@@ -118,7 +118,7 @@
-
+
This is a
TextBlock
with several
@@ -126,7 +126,7 @@
using a variety of styles
.
-
+
diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml
index 3377979199..76ecfcc622 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml
+++ b/samples/IntegrationTestApp/MainWindow.axaml
@@ -69,6 +69,23 @@
+
+
+
+ Reset
+
+
+
+
+
+
+
+
+
diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs
index f72f83fcb8..e8fb455c35 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml.cs
+++ b/samples/IntegrationTestApp/MainWindow.axaml.cs
@@ -4,6 +4,7 @@ using Avalonia;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
@@ -17,6 +18,7 @@ namespace IntegrationTestApp
{
InitializeComponent();
InitializeViewMenu();
+ InitializeGesturesTab();
this.AttachDevTools();
AddHandler(Button.ClickEvent, OnButtonClick);
ListBoxItems = Enumerable.Range(0, 100).Select(x => "Item " + x).ToList();
@@ -120,6 +122,38 @@ namespace IntegrationTestApp
}
}
+ private void InitializeGesturesTab()
+ {
+ var gestureBorder = this.GetControl("GestureBorder");
+ var gestureBorder2 = this.GetControl("GestureBorder2");
+ var lastGesture = this.GetControl("LastGesture");
+ var resetGestures = this.GetControl("ResetGestures");
+ gestureBorder.Tapped += (s, e) => lastGesture.Text = "Tapped";
+
+ gestureBorder.DoubleTapped += (s, e) =>
+ {
+ lastGesture.Text = "DoubleTapped";
+
+ // Testing #8733
+ gestureBorder.IsVisible = false;
+ gestureBorder2.IsVisible = true;
+ };
+
+ gestureBorder2.DoubleTapped += (s, e) =>
+ {
+ lastGesture.Text = "DoubleTapped2";
+ };
+
+ Gestures.AddRightTappedHandler(gestureBorder, (s, e) => lastGesture.Text = "RightTapped");
+
+ resetGestures.Click += (s, e) =>
+ {
+ lastGesture.Text = string.Empty;
+ gestureBorder.IsVisible = true;
+ gestureBorder2.IsVisible = false;
+ };
+ }
+
private void MenuClicked(object? sender, RoutedEventArgs e)
{
var clickedMenuItemTextBlock = this.Get("ClickedMenuItem");
diff --git a/samples/MobileSandbox/MainView.xaml b/samples/MobileSandbox/MainView.xaml
index 9c46f3eae9..5f9f41f3a9 100644
--- a/samples/MobileSandbox/MainView.xaml
+++ b/samples/MobileSandbox/MainView.xaml
@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+
diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs
index 040043e618..b6adbde738 100644
--- a/src/Android/Avalonia.Android/AndroidInputMethod.cs
+++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs
@@ -1,6 +1,7 @@
using System;
using Android.Content;
using Android.Runtime;
+using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
@@ -62,22 +63,21 @@ namespace Avalonia.Android
public void Reset()
{
- _imm.RestartInput(_host);
+
}
public void SetClient(ITextInputMethodClient client)
{
- if(client is null)
- {
- _inputConnection?.SetComposingText("", 0);
- }
-
if (_client != null)
{
_client.SurroundingTextChanged -= SurroundingTextChanged;
}
- Reset();
+ if(_inputConnection != null)
+ {
+ _inputConnection.ComposingText = null;
+ _inputConnection.ComposingRegion = default;
+ }
_client = client;
@@ -87,23 +87,31 @@ namespace Avalonia.Android
_host.RequestFocus();
+ _imm.RestartInput(View);
+
_imm.ShowSoftInput(_host, ShowFlags.Implicit);
- }
- else
- {
- _imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None);
+
+ var surroundingText = Client.SurroundingText;
+
+ _imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
}
private void SurroundingTextChanged(object sender, EventArgs e)
{
- if (IsActive)
+ if (IsActive && _inputConnection != null)
{
var surroundingText = Client.SurroundingText;
_inputConnection.SurroundingText = surroundingText;
_imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
+
+ if (_inputConnection.ComposingText != null && !_inputConnection.IsCommiting && surroundingText.AnchorOffset == surroundingText.CursorOffset)
+ {
+ _inputConnection.CommitText(_inputConnection.ComposingText, 0);
+ _inputConnection.SetSelection(surroundingText.AnchorOffset, surroundingText.CursorOffset);
+ }
}
}
@@ -153,10 +161,20 @@ namespace Avalonia.Android
return _inputConnection;
});
}
+ }
- private void RestoreSoftKeyboard(object sender, PointerReleasedEventArgs e)
+ public readonly struct ComposingRegion
+ {
+ private readonly int _start = -1;
+ private readonly int _end = -1;
+
+ public ComposingRegion(int start, int end)
{
- _imm.ShowSoftInput(_host, ShowFlags.Implicit);
+ _start = start;
+ _end = end;
}
+
+ public int Start => _start;
+ public int End => _end;
}
}
diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs
index ed5b46d398..2b6d29e7c5 100644
--- a/src/Android/Avalonia.Android/AndroidPlatform.cs
+++ b/src/Android/Avalonia.Android/AndroidPlatform.cs
@@ -26,21 +26,11 @@ namespace Avalonia
namespace Avalonia.Android
{
- class AndroidPlatform : IPlatformSettings
+ class AndroidPlatform
{
public static readonly AndroidPlatform Instance = new AndroidPlatform();
public static AndroidPlatformOptions Options { get; private set; }
- ///
- public Size TouchDoubleClickSize => new Size(4, 4);
-
- ///
- public TimeSpan TouchDoubleClickTime => TimeSpan.FromMilliseconds(200);
-
- public Size DoubleClickSize => TouchDoubleClickSize;
-
- public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500);
-
internal static Compositor Compositor { get; private set; }
public static void Initialize()
@@ -52,7 +42,7 @@ namespace Avalonia.Android
.Bind().ToTransient()
.Bind().ToConstant(new WindowingPlatformStub())
.Bind().ToSingleton()
- .Bind().ToConstant(Instance)
+ .Bind().ToSingleton()
.Bind().ToConstant(new AndroidThreadingInterface())
.Bind().ToSingleton()
.Bind().ToConstant(new ChoreographerTimer())
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
index 984eb775b5..5f53eb36c8 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
@@ -3,7 +3,9 @@ using System.Collections.Generic;
using Android.App;
using Android.Content;
using Android.Graphics;
+using Android.OS;
using Android.Runtime;
+using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.OpenGL;
@@ -23,6 +25,7 @@ using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Java.Lang;
+using Math = System.Math;
namespace Avalonia.Android.Platform.SkiaPlatform
{
@@ -165,6 +168,28 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_tl.Draw();
}
+ protected override void DispatchDraw(global::Android.Graphics.Canvas canvas)
+ {
+ // Workaround issue #9230 on where screen remains gray after splash screen.
+ // base.DispatchDraw should punch a hole into the canvas so the surface
+ // can be seen below, but it does not.
+ if (OperatingSystem.IsAndroidVersionAtLeast(29))
+ {
+ // Android 10+ does this (BlendMode was new)
+ var paint = new Paint();
+ paint.SetColor(0);
+ paint.BlendMode = BlendMode.Clear;
+ canvas.DrawRect(0, 0, Width, Height, paint);
+ }
+ else
+ {
+ // Android 9 did this
+ canvas.DrawColor(Color.Transparent, PorterDuff.Mode.Clear);
+ }
+
+ base.DispatchDraw(canvas);
+ }
+
protected override bool DispatchGenericPointerEvent(MotionEvent e)
{
bool callBase;
@@ -254,8 +279,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
throw new NotImplementedException();
}
-
-
}
internal class AvaloniaInputConnection : BaseInputConnection
@@ -271,11 +294,12 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public TextInputMethodSurroundingText SurroundingText { get; set; }
- public string ComposingText { get; private set; }
+ public string ComposingText { get; internal set; }
- public ComposingRegion ComposingRegion { get; private set; }
+ public ComposingRegion? ComposingRegion { get; internal set; }
- public bool IsComposing { get; private set; }
+ public bool IsComposing => !string.IsNullOrEmpty(ComposingText);
+ public bool IsCommiting { get; private set; }
public override bool SetComposingRegion(int start, int end)
{
@@ -292,8 +316,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
ComposingText = composingText;
- IsComposing = true;
-
_inputMethod.Client?.SetPreeditText(ComposingText);
return base.SetComposingText(text, newCursorPosition);
@@ -301,20 +323,25 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public override bool FinishComposingText()
{
- IsComposing = false;
-
- ComposingRegion = new ComposingRegion(SurroundingText.CursorOffset, SurroundingText.CursorOffset);
+ if (!string.IsNullOrEmpty(ComposingText))
+ {
+ CommitText(ComposingText, ComposingText.Length);
+ }
+ else
+ {
+ ComposingRegion = new ComposingRegion(SurroundingText.CursorOffset, SurroundingText.CursorOffset);
+ }
return base.FinishComposingText();
}
public override ICharSequence GetTextBeforeCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
{
- if (!string.IsNullOrEmpty(SurroundingText.Text))
+ if (!string.IsNullOrEmpty(SurroundingText.Text) && length > 0)
{
var start = System.Math.Max(SurroundingText.CursorOffset - length, 0);
- var end = System.Math.Min(start + length, SurroundingText.CursorOffset);
+ var end = System.Math.Min(start + length - 1, SurroundingText.CursorOffset);
var text = SurroundingText.Text.Substring(start, end - start);
@@ -346,11 +373,31 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public override bool CommitText(ICharSequence text, int newCursorPosition)
{
+ IsCommiting = true;
var committedText = text.ToString();
_inputMethod.Client.SetPreeditText(null);
- _inputMethod.Client.SelectInSurroundingText(ComposingRegion.Start, ComposingRegion.End);
+ int? start, end;
+
+ if(SurroundingText.CursorOffset != SurroundingText.AnchorOffset)
+ {
+ start = Math.Min(SurroundingText.CursorOffset, SurroundingText.AnchorOffset);
+ end = Math.Max(SurroundingText.CursorOffset, SurroundingText.AnchorOffset);
+ }
+ else if (ComposingRegion != null)
+ {
+ start = ComposingRegion?.Start;
+ end = ComposingRegion?.End;
+
+ ComposingRegion = null;
+ }
+ else
+ {
+ start = end = _inputMethod.Client.SurroundingText.CursorOffset;
+ }
+
+ _inputMethod.Client.SelectInSurroundingText((int)start, (int)end);
var time = DateTime.Now.TimeOfDay;
@@ -358,15 +405,29 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_topLevel.Input(rawTextEvent);
+ ComposingText = null;
+
+ ComposingRegion = new ComposingRegion(newCursorPosition, newCursorPosition);
+
return base.CommitText(text, newCursorPosition);
}
public override bool DeleteSurroundingText(int beforeLength, int afterLength)
{
- _inputMethod.Client.SelectInSurroundingText(beforeLength, afterLength);
+ var surroundingText = _inputMethod.Client.SurroundingText;
+
+ var selectionStart = surroundingText.CursorOffset;
+
+ _inputMethod.Client.SelectInSurroundingText(selectionStart - beforeLength, selectionStart + afterLength);
_inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
+ surroundingText = _inputMethod.Client.SurroundingText;
+
+ selectionStart = surroundingText.CursorOffset;
+
+ ComposingRegion = new ComposingRegion(selectionStart, selectionStart);
+
return base.DeleteSurroundingText(beforeLength, afterLength);
}
@@ -374,22 +435,23 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
_inputMethod.Client.SelectInSurroundingText(start, end);
+ ComposingRegion = new ComposingRegion(start, end);
+
return base.SetSelection(start, end);
}
- }
-
- public readonly struct ComposingRegion
- {
- private readonly int _start = -1;
- private readonly int _end = -1;
- public ComposingRegion(int start, int end)
+ public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode)
{
- _start = start;
- _end = end;
- }
+ switch (actionCode)
+ {
+ case ImeAction.Done:
+ {
+ _inputMethod.IMM.HideSoftInputFromWindow(_inputMethod.View.WindowToken, HideSoftInputFlags.ImplicitOnly);
+ break;
+ }
+ }
- public int Start => _start;
- public int End => _end;
+ return base.PerformEditorAction(actionCode);
+ }
}
}
diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
index 4cae700c0a..57f6469a42 100644
--- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
+++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
@@ -63,7 +63,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
{
var rawTextEvent = new RawTextInputEventArgs(
AndroidKeyboardDevice.Instance,
- Convert.ToUInt32(e.EventTime),
+ Convert.ToUInt64(e.EventTime),
_view.InputRoot,
unicodeTextInput ?? Convert.ToChar(e.UnicodeChar).ToString()
);
diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs
index 750fb263f5..35a391f2cb 100644
--- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs
@@ -81,7 +81,7 @@ namespace Avalonia.Collections
if (replace)
{
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]"));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));
if (CollectionChanged != null)
{
@@ -148,7 +148,7 @@ namespace Avalonia.Collections
{
_inner.Remove(key);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]"));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));
if (CollectionChanged != null)
{
@@ -208,7 +208,7 @@ namespace Avalonia.Collections
private void NotifyAdd(TKey key, TValue value)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]"));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));
if (CollectionChanged != null)
diff --git a/src/Avalonia.Base/Controls/NameScopeEventArgs.cs b/src/Avalonia.Base/Controls/NameScopeEventArgs.cs
deleted file mode 100644
index 3e9eaa6057..0000000000
--- a/src/Avalonia.Base/Controls/NameScopeEventArgs.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-
-namespace Avalonia.Controls
-{
- public class NameScopeEventArgs : EventArgs
- {
- public NameScopeEventArgs(string name, object element)
- {
- Name = name;
- Element = element;
- }
-
- public string Name { get; }
- public object Element { get; }
- }
-}
diff --git a/src/Avalonia.Base/Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs
index 0e613c0f21..41276e2f06 100644
--- a/src/Avalonia.Base/Input/DragEventArgs.cs
+++ b/src/Avalonia.Base/Input/DragEventArgs.cs
@@ -32,7 +32,7 @@ namespace Avalonia.Input
return point;
}
- public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
+ internal DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
: base(routedEvent)
{
Data = data;
diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs
index 4349fca7db..82ed96d982 100644
--- a/src/Avalonia.Base/Input/Gestures.cs
+++ b/src/Avalonia.Base/Input/Gestures.cs
@@ -1,5 +1,6 @@
using System;
using Avalonia.Interactivity;
+using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Input
@@ -42,9 +43,8 @@ namespace Avalonia.Input
RoutedEvent.Register(
"PointerSwipeGesture", RoutingStrategies.Bubble, typeof(Gestures));
-#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
- private static readonly WeakReference s_lastPress = new WeakReference(null);
-#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+ private static readonly WeakReference s_lastPress = new WeakReference(null);
+ private static Point s_lastPressPoint;
static Gestures()
{
@@ -94,10 +94,11 @@ namespace Avalonia.Input
var e = (PointerPressedEventArgs)ev;
var visual = (IVisual)ev.Source;
- if (e.ClickCount <= 1)
+ if (e.ClickCount % 2 == 1)
{
s_isDoubleTapped = false;
s_lastPress.SetTarget(ev.Source);
+ s_lastPressPoint = e.GetPosition((IVisual)ev.Source);
}
else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
{
@@ -107,10 +108,6 @@ namespace Avalonia.Input
e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e));
}
}
- else
- {
- s_isDoubleTapped = false;
- }
}
}
@@ -120,9 +117,17 @@ namespace Avalonia.Input
{
var e = (PointerReleasedEventArgs)ev;
- if (s_lastPress.TryGetTarget(out var target) && target == e.Source)
+ if (s_lastPress.TryGetTarget(out var target) &&
+ target == e.Source &&
+ e.InitialPressMouseButton is MouseButton.Left or MouseButton.Right)
{
- if (e.InitialPressMouseButton == MouseButton.Left || e.InitialPressMouseButton == MouseButton.Right)
+ var point = e.GetCurrentPoint((IVisual)target);
+ var settings = AvaloniaLocator.Current.GetService();
+ var tapSize = settings?.GetTapSize(point.Pointer.Type) ?? new Size(4, 4);
+ var tapRect = new Rect(s_lastPressPoint, new Size())
+ .Inflate(new Thickness(tapSize.Width, tapSize.Height));
+
+ if (tapRect.ContainsExclusive(point.Position))
{
if (e.InitialPressMouseButton == MouseButton.Right)
{
diff --git a/src/Avalonia.Base/Input/GotFocusEventArgs.cs b/src/Avalonia.Base/Input/GotFocusEventArgs.cs
index 5cce138ee0..f3de55ebae 100644
--- a/src/Avalonia.Base/Input/GotFocusEventArgs.cs
+++ b/src/Avalonia.Base/Input/GotFocusEventArgs.cs
@@ -7,6 +7,11 @@ namespace Avalonia.Input
///
public class GotFocusEventArgs : RoutedEventArgs
{
+ internal GotFocusEventArgs()
+ {
+
+ }
+
///
/// Gets or sets a value indicating how the change in focus occurred.
///
diff --git a/src/Avalonia.Base/Input/KeyEventArgs.cs b/src/Avalonia.Base/Input/KeyEventArgs.cs
index b8291e9096..39c9766105 100644
--- a/src/Avalonia.Base/Input/KeyEventArgs.cs
+++ b/src/Avalonia.Base/Input/KeyEventArgs.cs
@@ -5,6 +5,11 @@ namespace Avalonia.Input
{
public class KeyEventArgs : RoutedEventArgs
{
+ internal KeyEventArgs()
+ {
+
+ }
+
public IKeyboardDevice? Device { get; set; }
public Key Key { get; set; }
diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs
index 055c9cf1fd..b4bc419223 100644
--- a/src/Avalonia.Base/Input/MouseDevice.cs
+++ b/src/Avalonia.Base/Input/MouseDevice.cs
@@ -127,9 +127,9 @@ namespace Avalonia.Input
_pointer.Capture(source);
if (source != null)
{
- var settings = AvaloniaLocator.Current.GetService();
- var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500;
- var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4);
+ var settings = AvaloniaLocator.Current.GetRequiredService();
+ var doubleClickTime = settings.GetDoubleTapTime(PointerType.Mouse).TotalMilliseconds;
+ var doubleClickSize = settings.GetDoubleTapSize(PointerType.Mouse);
if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime)
{
diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs
index f5f0e90a45..31f34566be 100644
--- a/src/Avalonia.Base/Input/PenDevice.cs
+++ b/src/Avalonia.Base/Input/PenDevice.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
using Avalonia.Input.Raw;
using Avalonia.Platform;
@@ -77,8 +78,8 @@ namespace Avalonia.Input
{
pointer.Capture(source);
var settings = AvaloniaLocator.Current.GetService();
- var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500;
- var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4);
+ var doubleClickTime = settings?.GetDoubleTapTime(PointerType.Pen).TotalMilliseconds ?? 500;
+ var doubleClickSize = settings?.GetDoubleTapSize(PointerType.Pen) ?? new Size(4, 4);
if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime)
{
diff --git a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs
index b3085a038d..d5577d77af 100644
--- a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs
+++ b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs
@@ -7,7 +7,7 @@ namespace Avalonia.Input
{
public Vector Delta { get; set; }
- public PointerDeltaEventArgs(RoutedEvent routedEvent, IInteractive? source,
+ internal PointerDeltaEventArgs(RoutedEvent routedEvent, IInteractive? source,
IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(routedEvent, source, pointer, rootVisual, rootVisualPosition,
diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs
index a0e8d4a074..f84dec42cb 100644
--- a/src/Avalonia.Base/Input/PointerEventArgs.cs
+++ b/src/Avalonia.Base/Input/PointerEventArgs.cs
@@ -13,7 +13,7 @@ namespace Avalonia.Input
private readonly PointerPointProperties _properties;
private readonly Lazy?>? _previousPoints;
- public PointerEventArgs(RoutedEvent routedEvent,
+ internal PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source,
IPointer pointer,
IVisual? rootVisual, Point rootVisualPosition,
@@ -30,8 +30,8 @@ namespace Avalonia.Input
Timestamp = timestamp;
KeyModifiers = modifiers;
}
-
- public PointerEventArgs(RoutedEvent routedEvent,
+
+ internal PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source,
IPointer pointer,
IVisual? rootVisual, Point rootVisualPosition,
@@ -124,7 +124,7 @@ namespace Avalonia.Input
public class PointerPressedEventArgs : PointerEventArgs
{
- public PointerPressedEventArgs(
+ internal PointerPressedEventArgs(
IInteractive source,
IPointer pointer,
IVisual rootVisual, Point rootVisualPosition,
@@ -143,7 +143,7 @@ namespace Avalonia.Input
public class PointerReleasedEventArgs : PointerEventArgs
{
- public PointerReleasedEventArgs(
+ internal PointerReleasedEventArgs(
IInteractive source, IPointer pointer,
IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers,
@@ -164,7 +164,7 @@ namespace Avalonia.Input
{
public IPointer Pointer { get; }
- public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
+ internal PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
{
Pointer = pointer;
Source = source;
diff --git a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs
index e5701dcf23..dbc06ec934 100644
--- a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs
+++ b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs
@@ -7,7 +7,7 @@ namespace Avalonia.Input
{
public Vector Delta { get; set; }
- public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual,
+ internal PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual,
Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition,
diff --git a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
index a682e8f0a4..fd1d0f42c3 100644
--- a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
+++ b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
@@ -9,8 +9,8 @@ namespace Avalonia.Input
private static int _nextId = 1;
public static int GetNextFreeId() => _nextId++;
-
- public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
+
+ internal ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
{
Id = id;
Delta = delta;
@@ -21,7 +21,7 @@ namespace Avalonia.Input
{
public int Id { get; }
- public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
+ internal ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
{
Id = id;
}
diff --git a/src/Avalonia.Base/Input/TappedEventArgs.cs b/src/Avalonia.Base/Input/TappedEventArgs.cs
index daaab70632..8af6164fc1 100644
--- a/src/Avalonia.Base/Input/TappedEventArgs.cs
+++ b/src/Avalonia.Base/Input/TappedEventArgs.cs
@@ -7,7 +7,7 @@ namespace Avalonia.Input
{
private readonly PointerEventArgs lastPointerEventArgs;
- public TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs)
+ internal TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs)
: base(routedEvent)
{
this.lastPointerEventArgs = lastPointerEventArgs;
diff --git a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
index 325b6dd0ef..325d745782 100644
--- a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
+++ b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
@@ -29,6 +29,7 @@ namespace Avalonia.Input.TextInput
/// Sets the non-committed input string
///
void SetPreeditText(string? text);
+
///
/// Indicates if text input client is capable of providing the text around the cursor
///
diff --git a/src/Avalonia.Base/Input/TextInputEventArgs.cs b/src/Avalonia.Base/Input/TextInputEventArgs.cs
index cda0103749..787bf1abd3 100644
--- a/src/Avalonia.Base/Input/TextInputEventArgs.cs
+++ b/src/Avalonia.Base/Input/TextInputEventArgs.cs
@@ -4,6 +4,10 @@ namespace Avalonia.Input
{
public class TextInputEventArgs : RoutedEventArgs
{
+ internal TextInputEventArgs()
+ {
+
+ }
public IKeyboardDevice? Device { get; set; }
public string? Text { get; set; }
diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs
index 1d5b1d6bbf..c98944359d 100644
--- a/src/Avalonia.Base/Input/TouchDevice.cs
+++ b/src/Avalonia.Base/Input/TouchDevice.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
using Avalonia.Input.Raw;
using Avalonia.Platform;
@@ -59,16 +60,18 @@ namespace Avalonia.Input
else
{
var settings = AvaloniaLocator.Current.GetRequiredService();
+ var doubleClickTime = settings.GetDoubleTapTime(PointerType.Touch).TotalMilliseconds;
+ var doubleClickSize = settings.GetDoubleTapSize(PointerType.Touch);
if (!_lastClickRect.Contains(args.Position)
- || ev.Timestamp - _lastClickTime > settings.TouchDoubleClickTime.TotalMilliseconds)
+ || ev.Timestamp - _lastClickTime > doubleClickTime)
{
_clickCount = 0;
}
++_clickCount;
_lastClickTime = ev.Timestamp;
_lastClickRect = new Rect(args.Position, new Size())
- .Inflate(new Thickness(settings.TouchDoubleClickSize.Width / 2, settings.TouchDoubleClickSize.Height / 2));
+ .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2));
}
target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
diff --git a/src/Avalonia.Base/Input/VectorEventArgs.cs b/src/Avalonia.Base/Input/VectorEventArgs.cs
index 000fd52f69..3e8098f904 100644
--- a/src/Avalonia.Base/Input/VectorEventArgs.cs
+++ b/src/Avalonia.Base/Input/VectorEventArgs.cs
@@ -5,6 +5,11 @@ namespace Avalonia.Input
{
public class VectorEventArgs : RoutedEventArgs
{
+ internal VectorEventArgs()
+ {
+
+ }
+
public Vector Vector { get; set; }
}
}
diff --git a/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs b/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs
index ccbb41b7dc..2b660e7080 100644
--- a/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs
+++ b/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs
@@ -2,29 +2,59 @@ using System;
namespace Avalonia.Interactivity
{
+ ///
+ /// Provides state information and data specific to a routed event.
+ ///
public class RoutedEventArgs : EventArgs
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public RoutedEventArgs()
{
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The routed event associated with these event args.
public RoutedEventArgs(RoutedEvent? routedEvent)
{
RoutedEvent = routedEvent;
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The routed event associated with these event args.
+ /// The source object that raised the routed event.
public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
{
RoutedEvent = routedEvent;
Source = source;
}
+ ///
+ /// Gets or sets a value indicating whether the routed event has already been handled.
+ ///
+ ///
+ /// Once handled, a routed event should be ignored.
+ ///
public bool Handled { get; set; }
+ ///
+ /// Gets or sets the routed event associated with these event args.
+ ///
public RoutedEvent? RoutedEvent { get; set; }
+ ///
+ /// Gets or sets the routing strategy (direct, bubbling, or tunneling) of the routed event.
+ ///
public RoutingStrategies Route { get; set; }
+ ///
+ /// Gets or sets the source object that raised the routed event.
+ ///
public IInteractive? Source { get; set; }
}
}
diff --git a/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs b/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs
index 1cdc775b13..749d2ecc2b 100644
--- a/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs
+++ b/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs
@@ -7,7 +7,7 @@ namespace Avalonia.Layout
///
public class EffectiveViewportChangedEventArgs : EventArgs
{
- public EffectiveViewportChangedEventArgs(Rect effectiveViewport)
+ internal EffectiveViewportChangedEventArgs(Rect effectiveViewport)
{
EffectiveViewport = effectiveViewport;
}
diff --git a/src/Avalonia.Base/Layout/UniformGridLayoutState.cs b/src/Avalonia.Base/Layout/UniformGridLayoutState.cs
index d6b5a30bfc..65cdf4c94b 100644
--- a/src/Avalonia.Base/Layout/UniformGridLayoutState.cs
+++ b/src/Avalonia.Base/Layout/UniformGridLayoutState.cs
@@ -117,12 +117,12 @@ namespace Avalonia.Layout
double extraMinorPixelsForEachItem = 0.0;
if (!double.IsInfinity(availableSizeMinor))
{
- var numItemsPerColumn = Math.Min(
+ var numItemsPerColumn = (int)Math.Min(
maxItemsPerLine,
Math.Max(1.0, availableSizeMinor / (itemSizeMinor + minorItemSpacing)));
var usedSpace = (numItemsPerColumn * (itemSizeMinor + minorItemSpacing)) - minorItemSpacing;
- var remainingSpace = ((int)(availableSizeMinor - usedSpace));
- extraMinorPixelsForEachItem = remainingSpace / ((int)numItemsPerColumn);
+ var remainingSpace = availableSizeMinor - usedSpace;
+ extraMinorPixelsForEachItem = (int)(remainingSpace / numItemsPerColumn);
}
if (stretch == UniformGridLayoutItemsStretch.Fill)
diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs
index 27d99bdc10..90b9755493 100644
--- a/src/Avalonia.Base/Media/FormattedText.cs
+++ b/src/Avalonia.Base/Media/FormattedText.cs
@@ -93,7 +93,8 @@ namespace Avalonia.Media
runProps,
TextWrapping.WrapWithOverflow,
0, // line height not specified
- 0 // indentation not specified
+ 0, // indentation not specified
+ 0
);
InvalidateMetrics();
diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs
index a1cb00e209..d93a68e78b 100644
--- a/src/Avalonia.Base/Media/GlyphRun.cs
+++ b/src/Avalonia.Base/Media/GlyphRun.cs
@@ -170,7 +170,7 @@ namespace Avalonia.Media
}
///
- /// Gets the scale of the current
+ /// Gets the scale of the current
///
internal double Scale => FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight;
@@ -860,82 +860,9 @@ namespace Avalonia.Media
private IGlyphRunImpl CreateGlyphRunImpl()
{
- IGlyphRunImpl glyphRunImpl;
-
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService();
- var count = GlyphIndices.Count;
- var scale = (float)(FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight);
-
- if (GlyphOffsets == null)
- {
- if (GlyphTypeface.Metrics.IsFixedPitch)
- {
- var buffer = platformRenderInterface.AllocateGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count);
-
- var glyphs = buffer.GlyphIndices;
-
- for (int i = 0; i < glyphs.Length; i++)
- {
- glyphs[i] = GlyphIndices[i];
- }
-
- glyphRunImpl = buffer.Build();
- }
- else
- {
- var buffer = platformRenderInterface.AllocateHorizontalGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count);
- var glyphs = buffer.GlyphIndices;
- var positions = buffer.GlyphPositions;
- var width = 0d;
-
- for (var i = 0; i < count; i++)
- {
- positions[i] = (float)width;
-
- if (GlyphAdvances == null)
- {
- width += GlyphTypeface.GetGlyphAdvance(GlyphIndices[i]) * scale;
- }
- else
- {
- width += GlyphAdvances[i];
- }
-
- glyphs[i] = GlyphIndices[i];
- }
-
- glyphRunImpl = buffer.Build();
- }
- }
- else
- {
- var buffer = platformRenderInterface.AllocatePositionedGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count);
- var glyphs = buffer.GlyphIndices;
- var glyphPositions = buffer.GlyphPositions;
- var currentX = 0.0;
-
- for (var i = 0; i < count; i++)
- {
- var glyphOffset = GlyphOffsets[i];
-
- glyphPositions[i] = new PointF((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
-
- if (GlyphAdvances == null)
- {
- currentX += GlyphTypeface.GetGlyphAdvance(GlyphIndices[i]) * scale;
- }
- else
- {
- currentX += GlyphAdvances[i];
- }
-
- glyphs[i] = GlyphIndices[i];
- }
-
- glyphRunImpl = buffer.Build();
- }
- return glyphRunImpl;
+ return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
}
void IDisposable.Dispose()
diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs
index cf8a31c3e9..ce38fc5abc 100644
--- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs
+++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs
@@ -121,18 +121,28 @@ namespace Avalonia.Media.Imaging
/// Saves the bitmap to a file.
///
/// The filename.
- public void Save(string fileName)
+ ///
+ /// The optional quality for compression.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// setting is applied.
+ ///
+ public void Save(string fileName, int? quality = null)
{
- PlatformImpl.Item.Save(fileName);
+ PlatformImpl.Item.Save(fileName, quality);
}
///
/// Saves the bitmap to a stream.
///
/// The stream.
- public void Save(Stream stream)
+ ///
+ /// The optional quality for compression.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// setting is applied.
+ ///
+ public void Save(Stream stream, int? quality = null)
{
- PlatformImpl.Item.Save(stream);
+ PlatformImpl.Item.Save(stream, quality);
}
///
diff --git a/src/Avalonia.Base/Media/Imaging/IBitmap.cs b/src/Avalonia.Base/Media/Imaging/IBitmap.cs
index bd04d5ce86..e7d1862aa2 100644
--- a/src/Avalonia.Base/Media/Imaging/IBitmap.cs
+++ b/src/Avalonia.Base/Media/Imaging/IBitmap.cs
@@ -35,12 +35,22 @@ namespace Avalonia.Media.Imaging
/// Saves the bitmap to a file.
///
/// The filename.
- void Save(string fileName);
+ ///
+ /// The optional quality for compression if supported by the specific backend.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// setting of the backend is applied.
+ ///
+ void Save(string fileName, int? quality = null);
///
/// Saves the bitmap to a stream in png format.
///
/// The stream.
- void Save(Stream stream);
+ ///
+ /// The optional quality for compression if supported by the specific backend.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// setting of the backend is applied.
+ ///
+ void Save(Stream stream, int? quality = null);
}
}
diff --git a/src/Avalonia.Base/Media/PathMarkupParser.cs b/src/Avalonia.Base/Media/PathMarkupParser.cs
index cf12bf5126..5e808488fc 100644
--- a/src/Avalonia.Base/Media/PathMarkupParser.cs
+++ b/src/Avalonia.Base/Media/PathMarkupParser.cs
@@ -188,7 +188,11 @@ namespace Avalonia.Media
_isOpen = true;
}
- private void SetFillRule(scoped ref ReadOnlySpan span)
+ private void SetFillRule(
+#if NET7SDK
+ scoped
+#endif
+ ref ReadOnlySpan span)
{
ThrowIfDisposed();
@@ -452,7 +456,11 @@ namespace Avalonia.Media
return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || span[0] == '.' || char.IsDigit(span[0]));
}
- private static bool ReadArgument(scoped ref ReadOnlySpan remaining, out ReadOnlySpan argument)
+ private static bool ReadArgument(
+#if NET7SDK
+ scoped
+#endif
+ ref ReadOnlySpan remaining, out ReadOnlySpan argument)
{
remaining = SkipWhitespace(remaining);
if (remaining.IsEmpty)
diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
index 7ab67ea34d..fb8e699d8e 100644
--- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
@@ -140,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
}
}
- return length;
+ return Math.Min(length, text.Length);
}
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs b/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
index dccad1e647..b9ed31523e 100644
--- a/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
@@ -17,15 +17,18 @@
/// logical horizontal alignment
/// text wrap option
/// Paragraph line height
+ /// letter spacing
public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties,
TextAlignment textAlignment = TextAlignment.Left,
TextWrapping textWrap = TextWrapping.NoWrap,
- double lineHeight = 0)
+ double lineHeight = 0,
+ double letterSpacing = 0)
{
DefaultTextRunProperties = defaultTextRunProperties;
_textAlignment = textAlignment;
_textWrap = textWrap;
_lineHeight = lineHeight;
+ LetterSpacing = letterSpacing;
}
///
@@ -39,6 +42,7 @@
/// text wrap option
/// Paragraph line height
/// line indentation
+ /// letter spacing
public GenericTextParagraphProperties(
FlowDirection flowDirection,
TextAlignment textAlignment,
@@ -47,8 +51,8 @@
TextRunProperties defaultTextRunProperties,
TextWrapping textWrap,
double lineHeight,
- double indent
- )
+ double indent,
+ double letterSpacing)
{
_flowDirection = flowDirection;
_textAlignment = textAlignment;
@@ -57,6 +61,7 @@
DefaultTextRunProperties = defaultTextRunProperties;
_textWrap = textWrap;
_lineHeight = lineHeight;
+ LetterSpacing = letterSpacing;
Indent = indent;
}
@@ -72,7 +77,8 @@
textParagraphProperties.DefaultTextRunProperties,
textParagraphProperties.TextWrapping,
textParagraphProperties.LineHeight,
- textParagraphProperties.Indent)
+ textParagraphProperties.Indent,
+ textParagraphProperties.LetterSpacing)
{
}
@@ -131,6 +137,11 @@
///
public override double Indent { get; }
+ ///
+ /// The letter spacing
+ ///
+ public override double LetterSpacing { get; }
+
///
/// Set text flow direction
///
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
index 5c43a1c94f..7bad95c4a2 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
@@ -249,7 +249,8 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
currentRun.Properties.FontRenderingEmSize,
- shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab);
+ shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
+ paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
@@ -505,7 +506,7 @@ namespace Avalonia.Media.TextFormatting
case { } drawableTextRun:
{
- if (currentWidth + drawableTextRun.Size.Width > paragraphWidth)
+ if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{
goto found;
}
@@ -668,7 +669,7 @@ namespace Avalonia.Media.TextFormatting
if (!breakFound)
{
- currentLength += currentRun.Text.Length;
+ currentLength += currentRun.TextSourceLength;
continue;
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index 0828b6518a..dc79e61333 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -31,6 +31,7 @@ namespace Avalonia.Media.TextFormatting
/// The maximum width.
/// The maximum height.
/// The height of each line of text.
+ /// The letter spacing that is applied to rendered glyphs.
/// The maximum number of text lines.
/// The text style overrides.
public TextLayout(
@@ -46,12 +47,13 @@ namespace Avalonia.Media.TextFormatting
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
+ double letterSpacing = 0,
int maxLines = 0,
IReadOnlyList>? textStyleOverrides = null)
{
_paragraphProperties =
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
- textDecorations, flowDirection, lineHeight);
+ textDecorations, flowDirection, lineHeight, letterSpacing);
_textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
@@ -63,6 +65,8 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight;
+ LetterSpacing = letterSpacing;
+
MaxLines = maxLines;
TextLines = CreateTextLines();
@@ -77,6 +81,7 @@ namespace Avalonia.Media.TextFormatting
/// The maximum width.
/// The maximum height.
/// The height of each line of text.
+ /// The letter spacing that is applied to rendered glyphs.
/// The maximum number of text lines.
public TextLayout(
ITextSource textSource,
@@ -85,6 +90,7 @@ namespace Avalonia.Media.TextFormatting
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
+ double letterSpacing = 0,
int maxLines = 0)
{
_textSource = textSource;
@@ -99,6 +105,8 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight;
+ LetterSpacing = letterSpacing;
+
MaxLines = maxLines;
TextLines = CreateTextLines();
@@ -128,6 +136,11 @@ namespace Avalonia.Media.TextFormatting
///
public int MaxLines { get; }
+ ///
+ /// Gets the text spacing.
+ ///
+ public double LetterSpacing { get; }
+
///
/// Gets the text lines.
///
@@ -374,15 +387,17 @@ namespace Avalonia.Media.TextFormatting
/// The text decorations.
/// The text flow direction.
/// The height of each line of text.
+ /// The letter spacing that is applied to rendered glyphs.
///
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
- TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight)
+ TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight,
+ double letterSpacing)
{
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
return new GenericTextParagraphProperties(flowDirection, textAlignment, true, false,
- textRunStyle, textWrapping, lineHeight, 0);
+ textRunStyle, textWrapping, lineHeight, 0, letterSpacing);
}
///
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
index 82a0ba14d8..5691dd8ad0 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
@@ -57,7 +57,7 @@
public abstract double Indent { get; }
///
- /// Paragraph indentation
+ /// Get the paragraph indentation.
///
public virtual double ParagraphIndent
{
@@ -65,11 +65,16 @@
}
///
- /// Default Incremental Tab
+ /// Gets the default incremental tab width.
///
public virtual double DefaultIncrementalTab
{
get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; }
}
+
+ ///
+ /// Gets the letter spacing.
+ ///
+ public virtual double LetterSpacing { get; }
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
index 0d00bed51e..80bbbcdbfe 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
@@ -12,13 +12,15 @@ namespace Avalonia.Media.TextFormatting
double fontRenderingEmSize = 12,
sbyte bidiLevel = 0,
CultureInfo? culture = null,
- double incrementalTabWidth = 0)
+ double incrementalTabWidth = 0,
+ double letterSpacing = 0)
{
Typeface = typeface;
FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel;
Culture = culture;
IncrementalTabWidth = incrementalTabWidth;
+ LetterSpacing = letterSpacing;
}
///
@@ -45,5 +47,10 @@ namespace Avalonia.Media.TextFormatting
///
public double IncrementalTabWidth { get; }
+ ///
+ /// Get the letter spacing.
+ ///
+ public double LetterSpacing { get; }
+
}
}
diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
new file mode 100644
index 0000000000..dd3e1a4cb1
--- /dev/null
+++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
@@ -0,0 +1,30 @@
+using System;
+using Avalonia.Input;
+
+namespace Avalonia.Platform
+{
+ ///
+ /// A default implementation of for platforms which don't have
+ /// an OS-specific implementation.
+ ///
+ public class DefaultPlatformSettings : IPlatformSettings
+ {
+ public Size GetTapSize(PointerType type)
+ {
+ return type switch
+ {
+ PointerType.Touch => new(10, 10),
+ _ => new(4, 4),
+ };
+ }
+ public Size GetDoubleTapSize(PointerType type)
+ {
+ return type switch
+ {
+ PointerType.Touch => new(16, 16),
+ _ => new(4, 4),
+ };
+ }
+ public TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500);
+ }
+}
diff --git a/src/Avalonia.Base/Platform/IBitmapImpl.cs b/src/Avalonia.Base/Platform/IBitmapImpl.cs
index 8f11f68e7c..299a758961 100644
--- a/src/Avalonia.Base/Platform/IBitmapImpl.cs
+++ b/src/Avalonia.Base/Platform/IBitmapImpl.cs
@@ -29,12 +29,22 @@ namespace Avalonia.Platform
/// Saves the bitmap to a file.
///
/// The filename.
- void Save(string fileName);
+ ///
+ /// The optional quality for compression if supported by the specific backend.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// setting of the backend is applied.
+ ///
+ void Save(string fileName, int? quality = null);
///
/// Saves the bitmap to a stream in png format.
///
/// The stream.
- void Save(Stream stream);
+ ///
+ /// The optional quality for compression if supported by the specific backend.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// setting of the backend is applied.
+ ///
+ void Save(Stream stream, int? quality = null);
}
}
diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
index 9d0d7974b4..518c5f37b8 100644
--- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
@@ -171,40 +171,15 @@ namespace Avalonia.Platform
IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride);
///
- /// Allocates a platform glyph run buffer.
+ /// Creates a platform implementation of a glyph run.
///
/// The glyph typeface.
/// The font rendering em size.
- /// The length.
- /// An .
- ///
- /// This buffer only holds glyph indices.
- ///
- IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
-
- ///
- /// Allocates a horizontal platform glyph run buffer.
- ///
- /// The glyph typeface.
- /// The font rendering em size.
- /// The length.
- /// An .
- ///
- /// This buffer holds glyph indices and glyph advances.
- ///
- IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
-
- ///
- /// Allocates a positioned platform glyph run buffer.
- ///
- /// The glyph typeface.
- /// The font rendering em size.
- /// The length.
- /// An .
- ///
- /// This buffer holds glyph indices, glyph advances and glyph positions.
- ///
- IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
+ /// The glyph indices.
+ /// The glyph advances.
+ /// The glyph offsets.
+ ///
+ IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances, IReadOnlyList? glyphOffsets);
///
/// Gets a value indicating whether the platform directly supports rectangles with rounded corners.
diff --git a/src/Avalonia.Base/Platform/IPlatformSettings.cs b/src/Avalonia.Base/Platform/IPlatformSettings.cs
index 78d1817312..e7921883fd 100644
--- a/src/Avalonia.Base/Platform/IPlatformSettings.cs
+++ b/src/Avalonia.Base/Platform/IPlatformSettings.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Input;
using Avalonia.Metadata;
namespace Avalonia.Platform
@@ -6,18 +7,25 @@ namespace Avalonia.Platform
[Unstable]
public interface IPlatformSettings
{
- Size DoubleClickSize { get; }
-
- TimeSpan DoubleClickTime { get; }
+ ///
+ /// The size of the rectangle around the location of a pointer down that a pointer up
+ /// must occur within in order to register a tap gesture, in device-independent pixels.
+ ///
+ /// The pointer type.
+ Size GetTapSize(PointerType type);
///
- /// Determines the size of the area within that you should click twice in order for a double click to be counted.
+ /// The size of the rectangle around the location of a pointer down that a pointer up
+ /// must occur within in order to register a double-tap gesture, in device-independent
+ /// pixels.
///
- Size TouchDoubleClickSize { get; }
+ /// The pointer type.
+ Size GetDoubleTapSize(PointerType type);
///
- /// Determines the time span that what will be used to determine the double-click.
+ /// Gets the maximum time that may occur between the first and second click of a double-
+ /// tap gesture.
///
- TimeSpan TouchDoubleClickTime { get; }
+ TimeSpan GetDoubleTapTime(PointerType type);
}
}
diff --git a/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs
index cac4d1693a..73840376fe 100644
--- a/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs
+++ b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Rendering
///
/// The render root that has been updated.
/// The updated area.
- public SceneInvalidatedEventArgs(
+ internal SceneInvalidatedEventArgs(
IRenderRoot root,
Rect dirtyRect)
{
diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs
index ee176a6b85..76e6459e2e 100644
--- a/src/Avalonia.Base/Utilities/IdentifierParser.cs
+++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs
@@ -8,7 +8,11 @@ namespace Avalonia.Utilities
#endif
static class IdentifierParser
{
- public static ReadOnlySpan ParseIdentifier(this scoped ref CharacterReader r)
+ public static ReadOnlySpan ParseIdentifier(this
+#if NET7SDK
+ scoped
+#endif
+ ref CharacterReader r)
{
if (IsValidIdentifierStart(r.Peek))
{
diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs
index 8fa7ede77e..d009926bc5 100644
--- a/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs
+++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs
@@ -42,6 +42,11 @@ namespace Avalonia.Controls
Red800 = 0xFFC62828,
Red900 = 0xFFB71C1C,
+ RedA100 = 0xFFFF8A80,
+ RedA200 = 0xFFFF5252,
+ RedA400 = 0xFFFF1744,
+ RedA700 = 0xFFD50000,
+
// Pink
Pink50 = 0xFFFCE4EC,
Pink100 = 0xFFF8BBD0,
@@ -54,6 +59,11 @@ namespace Avalonia.Controls
Pink800 = 0xFFAD1457,
Pink900 = 0xFF880E4F,
+ PinkA100 = 0xFFFF80AB,
+ PinkA200 = 0xFFFF4081,
+ PinkA400 = 0xFFF50057,
+ PinkA700 = 0xFFC51162,
+
// Purple
Purple50 = 0xFFF3E5F5,
Purple100 = 0xFFE1BEE7,
@@ -66,6 +76,11 @@ namespace Avalonia.Controls
Purple800 = 0xFF6A1B9A,
Purple900 = 0xFF4A148C,
+ PurpleA100 = 0xFFEA80FC,
+ PurpleA200 = 0xFFE040FB,
+ PurpleA400 = 0xFFD500F9,
+ PurpleA700 = 0xFFAA00FF,
+
// Deep Purple
DeepPurple50 = 0xFFEDE7F6,
DeepPurple100 = 0xFFD1C4E9,
@@ -78,6 +93,11 @@ namespace Avalonia.Controls
DeepPurple800 = 0xFF4527A0,
DeepPurple900 = 0xFF311B92,
+ DeepPurpleA100 = 0xFFB388FF,
+ DeepPurpleA200 = 0xFF7C4DFF,
+ DeepPurpleA400 = 0xFF651FFF,
+ DeepPurpleA700 = 0xFF6200EA,
+
// Indigo
Indigo50 = 0xFFE8EAF6,
Indigo100 = 0xFFC5CAE9,
@@ -90,6 +110,11 @@ namespace Avalonia.Controls
Indigo800 = 0xFF283593,
Indigo900 = 0xFF1A237E,
+ IndigoA100 = 0xFF8C9EFF,
+ IndigoA200 = 0xFF536DFE,
+ IndigoA400 = 0xFF3D5AFE,
+ IndigoA700 = 0xFF304FFE,
+
// Blue
Blue50 = 0xFFE3F2FD,
Blue100 = 0xFFBBDEFB,
@@ -102,6 +127,11 @@ namespace Avalonia.Controls
Blue800 = 0xFF1565C0,
Blue900 = 0xFF0D47A1,
+ BlueA100 = 0xFF82B1FF,
+ BlueA200 = 0xFF448AFF,
+ BlueA400 = 0xFF2979FF,
+ BlueA700 = 0xFF2962FF,
+
// Light Blue
LightBlue50 = 0xFFE1F5FE,
LightBlue100 = 0xFFB3E5FC,
@@ -114,6 +144,11 @@ namespace Avalonia.Controls
LightBlue800 = 0xFF0277BD,
LightBlue900 = 0xFF01579B,
+ LightBlueA100 = 0xFF80D8FF,
+ LightBlueA200 = 0xFF40C4FF,
+ LightBlueA400 = 0xFF00B0FF,
+ LightBlueA700 = 0xFF0091EA,
+
// Cyan
Cyan50 = 0xFFE0F7FA,
Cyan100 = 0xFFB2EBF2,
@@ -126,6 +161,11 @@ namespace Avalonia.Controls
Cyan800 = 0xFF00838F,
Cyan900 = 0xFF006064,
+ CyanA100 = 0xFF84FFFF,
+ CyanA200 = 0xFF18FFFF,
+ CyanA400 = 0xFF00E5FF,
+ CyanA700 = 0xFF00B8D4,
+
// Teal
Teal50 = 0xFFE0F2F1,
Teal100 = 0xFFB2DFDB,
@@ -138,6 +178,11 @@ namespace Avalonia.Controls
Teal800 = 0xFF00695C,
Teal900 = 0xFF004D40,
+ TealA100 = 0xFFA7FFEB,
+ TealA200 = 0xFF64FFDA,
+ TealA400 = 0xFF1DE9B6,
+ TealA700 = 0xFF00BFA5,
+
// Green
Green50 = 0xFFE8F5E9,
Green100 = 0xFFC8E6C9,
@@ -150,6 +195,11 @@ namespace Avalonia.Controls
Green800 = 0xFF2E7D32,
Green900 = 0xFF1B5E20,
+ GreenA100 = 0xFFB9F6CA,
+ GreenA200 = 0xFF69F0AE,
+ GreenA400 = 0xFF00E676,
+ GreenA700 = 0xFF00C853,
+
// Light Green
LightGreen50 = 0xFFF1F8E9,
LightGreen100 = 0xFFDCEDC8,
@@ -162,6 +212,11 @@ namespace Avalonia.Controls
LightGreen800 = 0xFF558B2F,
LightGreen900 = 0xFF33691E,
+ LightGreenA100 = 0xFFCCFF90,
+ LightGreenA200 = 0xFFB2FF59,
+ LightGreenA400 = 0xFF76FF03,
+ LightGreenA700 = 0xFF64DD17,
+
// Lime
Lime50 = 0xFFF9FBE7,
Lime100 = 0xFFF0F4C3,
@@ -174,6 +229,11 @@ namespace Avalonia.Controls
Lime800 = 0xFF9E9D24,
Lime900 = 0xFF827717,
+ LimeA100 = 0xFFF4FF81,
+ LimeA200 = 0xFFEEFF41,
+ LimeA400 = 0xFFC6FF00,
+ LimeA700 = 0xFFAEEA00,
+
// Yellow
Yellow50 = 0xFFFFFDE7,
Yellow100 = 0xFFFFF9C4,
@@ -186,6 +246,11 @@ namespace Avalonia.Controls
Yellow800 = 0xFFF9A825,
Yellow900 = 0xFFF57F17,
+ YellowA100 = 0xFFFFFF8D,
+ YellowA200 = 0xFFFFFF00,
+ YellowA400 = 0xFFFFEA00,
+ YellowA700 = 0xFFFFD600,
+
// Amber
Amber50 = 0xFFFFF8E1,
Amber100 = 0xFFFFECB3,
@@ -198,6 +263,11 @@ namespace Avalonia.Controls
Amber800 = 0xFFFF8F00,
Amber900 = 0xFFFF6F00,
+ AmberA100 = 0xFFFFE57F,
+ AmberA200 = 0xFFFFD740,
+ AmberA400 = 0xFFFFC400,
+ AmberA700 = 0xFFFFAB00,
+
// Orange
Orange50 = 0xFFFFF3E0,
Orange100 = 0xFFFFE0B2,
@@ -210,6 +280,11 @@ namespace Avalonia.Controls
Orange800 = 0xFFEF6C00,
Orange900 = 0xFFE65100,
+ OrangeA100 = 0xFFFFD180,
+ OrangeA200 = 0xFFFFAB40,
+ OrangeA400 = 0xFFFF9100,
+ OrangeA700 = 0xFFFF6D00,
+
// Deep Orange
DeepOrange50 = 0xFFFBE9E7,
DeepOrange100 = 0xFFFFCCBC,
@@ -222,6 +297,11 @@ namespace Avalonia.Controls
DeepOrange800 = 0xFFD84315,
DeepOrange900 = 0xFFBF360C,
+ DeepOrangeA100 = 0xFFFF9E80,
+ DeepOrangeA200 = 0xFFFF6E40,
+ DeepOrangeA400 = 0xFFFF3D00,
+ DeepOrangeA700 = 0xFFDD2C00,
+
// Brown
Brown50 = 0xFFEFEBE9,
Brown100 = 0xFFD7CCC8,
@@ -257,6 +337,9 @@ namespace Avalonia.Controls
BlueGray700 = 0xFF455A64,
BlueGray800 = 0xFF37474F,
BlueGray900 = 0xFF263238,
+
+ Black = 0xFF000000,
+ White = 0xFFFFFFFF,
}
// See: https://material.io/design/color/the-color-system.html#tools-for-picking-colors
diff --git a/src/Avalonia.Controls/Automation/AutomationProperties.cs b/src/Avalonia.Controls/Automation/AutomationProperties.cs
index c20af148b8..a192ef6420 100644
--- a/src/Avalonia.Controls/Automation/AutomationProperties.cs
+++ b/src/Avalonia.Controls/Automation/AutomationProperties.cs
@@ -9,6 +9,11 @@ namespace Avalonia.Automation
///
public enum AccessibilityView
{
+ ///
+ /// The control's view is defined by its automation peer.
+ ///
+ Default,
+
///
/// The control is included in the Raw view of the automation tree.
///
@@ -44,8 +49,7 @@ namespace Avalonia.Automation
public static readonly AttachedProperty AccessibilityViewProperty =
AvaloniaProperty.RegisterAttached(
"AccessibilityView",
- typeof(AutomationProperties),
- defaultValue: AccessibilityView.Content);
+ typeof(AutomationProperties));
///
/// Defines the AutomationProperties.AccessKey attached property
diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
index 71421ac136..3d3fe35d29 100644
--- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
@@ -128,13 +128,13 @@ namespace Avalonia.Automation.Peers
/// Gets a value that indicates whether the element that is associated with this automation
/// peer contains data that is presented to the user.
///
- public bool IsContentElement() => IsControlElement() && IsContentElementCore();
+ public bool IsContentElement() => IsContentElementOverrideCore();
///
/// Gets a value that indicates whether the element is understood by the user as
/// interactive or as contributing to the logical structure of the control in the GUI.
///
- public bool IsControlElement() => IsControlElementCore();
+ public bool IsControlElement() => IsControlElementOverrideCore();
///
/// Gets a value indicating whether the control is enabled for user interaction.
@@ -247,6 +247,16 @@ namespace Avalonia.Automation.Peers
return GetAutomationControlTypeCore();
}
+ protected virtual bool IsContentElementOverrideCore()
+ {
+ return IsControlElement() && IsContentElementCore();
+ }
+
+ protected virtual bool IsControlElementOverrideCore()
+ {
+ return IsControlElementCore();
+ }
+
protected virtual object? GetProviderCore(Type providerType)
{
return providerType.IsAssignableFrom(this.GetType()) ? this : null;
diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
index a93d3fa7dd..25172b2d1c 100644
--- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
@@ -149,8 +149,8 @@ namespace Avalonia.Automation.Peers
protected override Rect GetBoundingRectangleCore() => GetBounds(Owner);
protected override string GetClassNameCore() => Owner.GetType().Name;
protected override bool HasKeyboardFocusCore() => Owner.IsFocused;
- protected override bool IsContentElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Content;
- protected override bool IsControlElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Control;
+ protected override bool IsContentElementCore() => true;
+ protected override bool IsControlElementCore() => true;
protected override bool IsEnabledCore() => Owner.IsEnabled;
protected override bool IsKeyboardFocusableCore() => Owner.Focusable;
protected override void SetFocusCore() => Owner.Focus();
@@ -160,6 +160,18 @@ namespace Avalonia.Automation.Peers
return AutomationProperties.GetControlTypeOverride(Owner) ?? GetAutomationControlTypeCore();
}
+ protected override bool IsContentElementOverrideCore()
+ {
+ var view = AutomationProperties.GetAccessibilityView(Owner);
+ return view == AccessibilityView.Default ? IsContentElementCore() : view >= AccessibilityView.Content;
+ }
+
+ protected override bool IsControlElementOverrideCore()
+ {
+ var view = AutomationProperties.GetAccessibilityView(Owner);
+ return view == AccessibilityView.Default ? IsControlElementCore() : view >= AccessibilityView.Control;
+ }
+
private static Rect GetBounds(Control control)
{
var root = control.GetVisualRoot();
diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs
index 524362fcf9..beaee34b07 100644
--- a/src/Avalonia.Controls/Control.cs
+++ b/src/Avalonia.Controls/Control.cs
@@ -84,6 +84,13 @@ namespace Avalonia.Controls
nameof(Unloaded),
RoutingStrategies.Direct);
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent SizeChangedEvent =
+ RoutedEvent.Register(
+ nameof(SizeChanged), RoutingStrategies.Direct);
+
///
/// Defines the property.
///
@@ -211,6 +218,15 @@ namespace Avalonia.Controls
remove => RemoveHandler(UnloadedEvent, value);
}
+ ///
+ /// Occurs when the bounds (actual size) of the control have changed.
+ ///
+ public event EventHandler? SizeChanged
+ {
+ add => AddHandler(SizeChangedEvent, value);
+ remove => RemoveHandler(SizeChangedEvent, value);
+ }
+
public new IControl? Parent => (IControl?)base.Parent;
///
@@ -530,14 +546,35 @@ namespace Avalonia.Controls
}
}
+ ///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
- if (change.Property == FlowDirectionProperty)
+ if (change.Property == BoundsProperty)
+ {
+ var oldValue = change.GetOldValue();
+ var newValue = change.GetNewValue();
+
+ // Bounds is a Rect with an X/Y Position as well as Height/Width.
+ // This means it is possible for the Rect to change position but not size.
+ // Therefore, we want to explicity check only the size and raise an event
+ // only when that size has changed.
+ if (newValue.Size != oldValue.Size)
+ {
+ var sizeChangedEventArgs = new SizeChangedEventArgs(
+ SizeChangedEvent,
+ source: this,
+ previousSize: new Size(oldValue.Width, oldValue.Height),
+ newSize: new Size(newValue.Width, newValue.Height));
+
+ RaiseEvent(sizeChangedEventArgs);
+ }
+ }
+ else if (change.Property == FlowDirectionProperty)
{
InvalidateMirrorTransform();
-
+
foreach (var visual in VisualChildren)
{
if (visual is Control child)
diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs
index 1ba65b3e8f..5b68402f87 100644
--- a/src/Avalonia.Controls/Documents/InlineCollection.cs
+++ b/src/Avalonia.Controls/Documents/InlineCollection.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList
{
- private ILogical? _parent;
+ private IAvaloniaList? _logicalChildren;
private IInlineHost? _inlineHost;
///
@@ -24,28 +24,30 @@ namespace Avalonia.Controls.Documents
this.ForEachItem(
x =>
- {
- ((ISetLogicalParent)x).SetParent(Parent);
+ {
x.InlineHost = InlineHost;
+ LogicalChildren?.Add(x);
Invalidate();
},
x =>
{
- ((ISetLogicalParent)x).SetParent(null);
+ LogicalChildren?.Remove(x);
x.InlineHost = InlineHost;
Invalidate();
},
() => throw new NotSupportedException());
}
- internal ILogical? Parent
+ internal IAvaloniaList? LogicalChildren
{
- get => _parent;
+ get => _logicalChildren;
set
{
- _parent = value;
+ var oldValue = _logicalChildren;
+
+ _logicalChildren = value;
- OnParentChanged(value);
+ OnParentChanged(oldValue, value);
}
}
@@ -70,6 +72,11 @@ namespace Avalonia.Controls.Documents
{
get
{
+ if (Count == 0)
+ {
+ return null;
+ }
+
var builder = StringBuilderCache.Acquire();
foreach (var inline in this)
@@ -82,56 +89,45 @@ namespace Avalonia.Controls.Documents
}
+ public override void Add(Inline inline)
+ {
+ if (InlineHost is TextBlock textBlock && !string.IsNullOrEmpty(textBlock._text))
+ {
+ base.Add(new Run(textBlock._text));
+
+ textBlock._text = null;
+ }
+
+ base.Add(inline);
+ }
+
///
- /// Add a text segment to the collection.
+ /// Adds a text segment to the collection.
///
/// For non complex content this appends the text to the end of currently held text.
/// For complex content this adds a to the collection.
///
///
- ///
+ /// The to be added text.
public void Add(string text)
{
- AddText(text);
- }
-
- public override void Add(Inline inline)
- {
- OnAdd();
-
- base.Add(inline);
- }
-
- public void Add(IControl child)
- {
- OnAdd();
-
- base.Add(new InlineUIContainer(child));
- }
-
- private void AddText(string text)
- {
- if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent)
+ if (InlineHost is TextBlock textBlock && !textBlock.HasComplexContent)
{
textBlock._text += text;
}
else
{
- base.Add(new Run(text));
+ Add(new Run(text));
}
}
- private void OnAdd()
+ ///
+ /// Adds a control wrapped inside a to the collection.
+ ///
+ /// The to be added control.
+ public void Add(IControl control)
{
- if (Parent is RichTextBlock textBlock)
- {
- if (!textBlock.HasComplexContent && !string.IsNullOrEmpty(textBlock._text))
- {
- base.Add(new Run(textBlock._text));
-
- textBlock._text = null;
- }
- }
+ Add(new InlineUIContainer(control));
}
///
@@ -152,20 +148,21 @@ namespace Avalonia.Controls.Documents
Invalidated?.Invoke(this, EventArgs.Empty);
}
- private void OnParentChanged(ILogical? parent)
+ private void OnParentChanged(IAvaloniaList? oldParent, IAvaloniaList? newParent)
{
foreach (var child in this)
{
- var oldParent = child.Parent;
-
- if (oldParent != parent)
+ if (oldParent != newParent)
{
if (oldParent != null)
{
- ((ISetLogicalParent)child).SetParent(null);
+ oldParent.Remove(child);
}
- ((ISetLogicalParent)child).SetParent(parent);
+ if(newParent != null)
+ {
+ newParent.Add(child);
+ }
}
}
}
diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs
index 363ce1011b..a7a702ceae 100644
--- a/src/Avalonia.Controls/Documents/Span.cs
+++ b/src/Avalonia.Controls/Documents/Span.cs
@@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents
{
Inlines = new InlineCollection
{
- Parent = this
+ LogicalChildren = LogicalChildren
};
}
@@ -78,14 +78,14 @@ namespace Avalonia.Controls.Documents
{
if (oldValue is not null)
{
- oldValue.Parent = null;
+ oldValue.LogicalChildren = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate();
}
if (newValue is not null)
{
- newValue.Parent = this;
+ newValue.LogicalChildren = LogicalChildren;
newValue.InlineHost = InlineHost;
newValue.Invalidated += (s, e) => InlineHost?.Invalidate();
}
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index a9bb16c7df..cc1fa6c513 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -9,6 +9,7 @@ using Avalonia.VisualTree;
using Avalonia.Layout;
using Avalonia.Media.Immutable;
using Avalonia.Controls.Documents;
+using Avalonia.Input.TextInput;
namespace Avalonia.Controls.Presenters
{
@@ -80,6 +81,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner();
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty LetterSpacingProperty =
+ TextBlock.LetterSpacingProperty.AddOwner();
+
///
/// Defines the property.
///
@@ -212,6 +219,15 @@ namespace Avalonia.Controls.Presenters
set => SetValue(LineHeightProperty, value);
}
+ ///
+ /// Gets or sets the letter spacing.
+ ///
+ public double LetterSpacing
+ {
+ get => GetValue(LetterSpacingProperty);
+ set => SetValue(LetterSpacingProperty, value);
+ }
+
///
/// Gets or sets the text alignment.
///
@@ -333,7 +349,7 @@ namespace Avalonia.Controls.Presenters
var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment,
TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
- flowDirection: FlowDirection, lineHeight: LineHeight);
+ flowDirection: FlowDirection, lineHeight: LineHeight, letterSpacing: LetterSpacing);
return textLayout;
}
@@ -499,7 +515,12 @@ namespace Avalonia.Controls.Presenters
{
if (!string.IsNullOrEmpty(_preeditText))
{
- var text = _text?.Substring(0, _caretIndex) + _preeditText + _text?.Substring(_caretIndex);
+ if (string.IsNullOrEmpty(_text) || _caretIndex > _text.Length)
+ {
+ return _preeditText;
+ }
+
+ var text = _text.Substring(0, _caretIndex) + _preeditText + _text.Substring(_caretIndex);
return text;
}
@@ -853,28 +874,11 @@ namespace Avalonia.Controls.Presenters
if (string.IsNullOrEmpty(newValue))
{
- if (!string.IsNullOrEmpty(oldValue))
- {
- var textPosition = _compositionStartHit.FirstCharacterIndex + _compositionStartHit.TrailingLength + newValue?.Length ?? 0;
-
- var characterHit = GetCharacterHitFromTextPosition(textPosition);
-
- UpdateCaret(characterHit, true);
- }
-
- _compositionStartHit = new CharacterHit(-1);
+ UpdateCaret(_lastCharacterHit);
}
else
{
- if (_compositionStartHit.FirstCharacterIndex == -1)
- {
- _compositionStartHit = _lastCharacterHit;
- }
- }
-
- if (_compositionStartHit.FirstCharacterIndex != -1)
- {
- var textPosition = _compositionStartHit.FirstCharacterIndex + _compositionStartHit.TrailingLength + newValue?.Length ?? 0;
+ var textPosition = _caretIndex + newValue?.Length ?? 0;
var characterHit = GetCharacterHitFromTextPosition(textPosition);
@@ -916,6 +920,9 @@ namespace Avalonia.Controls.Presenters
case nameof(TextAlignment):
case nameof(TextWrapping):
+ case nameof(LineHeight):
+ case nameof(LetterSpacing):
+
case nameof(SelectionStart):
case nameof(SelectionEnd):
case nameof(SelectionForegroundBrush):
diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs
index 1e406157d7..71a7a58da4 100644
--- a/src/Avalonia.Controls/ProgressBar.cs
+++ b/src/Avalonia.Controls/ProgressBar.cs
@@ -110,7 +110,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty ProgressTextFormatProperty =
AvaloniaProperty.Register(nameof(ProgressTextFormat), "{1:0}%");
-
+
public static readonly StyledProperty OrientationProperty =
AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal);
@@ -136,7 +136,7 @@ namespace Avalonia.Controls
get { return _percentage; }
private set { SetAndRaise(PercentageProperty, ref _percentage, value); }
}
-
+
public double IndeterminateStartingOffset
{
get => _indeterminateStartingOffset;
@@ -156,6 +156,7 @@ namespace Avalonia.Controls
MinimumProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e));
MaximumProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e));
IsIndeterminateProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e));
+ OrientationProperty.Changed.AddClassHandler((x, e) => x.UpdateIndicatorWhenPropChanged(e));
}
public ProgressBar()
@@ -216,13 +217,13 @@ namespace Avalonia.Controls
{
// dispose any previous track size listener
_trackSizeChangedListener?.Dispose();
-
+
_indicator = e.NameScope.Get("PART_Indicator");
// listen to size changes of the indicators track (parent) and update the indicator there.
_trackSizeChangedListener = _indicator.Parent?.GetPropertyChangedObservable(BoundsProperty)
.Subscribe(_ => UpdateIndicator());
-
+
UpdateIndicator();
}
@@ -230,7 +231,7 @@ namespace Avalonia.Controls
{
// Gets the size of the parent indicator container
var barSize = _indicator?.Parent?.Bounds.Size ?? Bounds.Size;
-
+
if (_indicator != null)
{
if (IsIndeterminate)
@@ -268,10 +269,18 @@ namespace Avalonia.Controls
{
double percent = Maximum == Minimum ? 1.0 : (Value - Minimum) / (Maximum - Minimum);
+ // When the Orientation changed, the indicator's Width or Height should set to double.NaN.
if (Orientation == Orientation.Horizontal)
+ {
_indicator.Width = barSize.Width * percent;
+ _indicator.Height = double.NaN;
+ }
else
+ {
+ _indicator.Width = double.NaN;
_indicator.Height = barSize.Height * percent;
+ }
+
Percentage = percent * 100;
}
diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs
similarity index 55%
rename from src/Avalonia.Controls/RichTextBlock.cs
rename to src/Avalonia.Controls/SelectableTextBlock.cs
index d0b713ba56..b343439f98 100644
--- a/src/Avalonia.Controls/RichTextBlock.cs
+++ b/src/Avalonia.Controls/SelectableTextBlock.cs
@@ -8,7 +8,6 @@ using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
-using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls
@@ -16,67 +15,53 @@ namespace Avalonia.Controls
///
/// A control that displays a block of formatted text.
///
- public class RichTextBlock : TextBlock, IInlineHost
+ public class SelectableTextBlock : TextBlock, IInlineHost
{
- public static readonly StyledProperty IsTextSelectionEnabledProperty =
- AvaloniaProperty.Register(nameof(IsTextSelectionEnabled), false);
-
- public static readonly DirectProperty SelectionStartProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty SelectionStartProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(SelectionStart),
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
- public static readonly DirectProperty SelectionEndProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty SelectionEndProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
- public static readonly DirectProperty SelectedTextProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty SelectedTextProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(SelectedText),
o => o.SelectedText);
public static readonly StyledProperty SelectionBrushProperty =
- AvaloniaProperty.Register(nameof(SelectionBrush), Brushes.Blue);
+ AvaloniaProperty.Register(nameof(SelectionBrush), Brushes.Blue);
- ///
- /// Defines the property.
- ///
- public static readonly StyledProperty InlinesProperty =
- AvaloniaProperty.Register(
- nameof(Inlines));
- public static readonly DirectProperty CanCopyProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty CanCopyProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(CanCopy),
o => o.CanCopy);
public static readonly RoutedEvent CopyingToClipboardEvent =
- RoutedEvent.Register(
+ RoutedEvent.Register(
nameof(CopyingToClipboard), RoutingStrategies.Bubble);
private bool _canCopy;
private int _selectionStart;
private int _selectionEnd;
private int _wordSelectionStart = -1;
- private IReadOnlyList? _textRuns;
- static RichTextBlock()
+ static SelectableTextBlock()
{
- FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
-
- AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty);
+ FocusableProperty.OverrideDefaultValue(typeof(SelectableTextBlock), true);
+ AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty);
}
- public RichTextBlock()
+ public event EventHandler? CopyingToClipboard
{
- Inlines = new InlineCollection
- {
- Parent = this,
- InlineHost = this
- };
+ add => AddHandler(CopyingToClipboardEvent, value);
+ remove => RemoveHandler(CopyingToClipboardEvent, value);
}
///
@@ -99,6 +84,8 @@ namespace Avalonia.Controls
if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
+
+ UpdateCommandStates();
}
}
}
@@ -114,6 +101,8 @@ namespace Avalonia.Controls
if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
+
+ UpdateCommandStates();
}
}
}
@@ -126,25 +115,6 @@ namespace Avalonia.Controls
get => GetSelection();
}
- ///
- /// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API.
- ///
- public bool IsTextSelectionEnabled
- {
- get => GetValue(IsTextSelectionEnabledProperty);
- set => SetValue(IsTextSelectionEnabledProperty, value);
- }
-
- ///
- /// Gets or sets the inlines.
- ///
- [Content]
- public InlineCollection? Inlines
- {
- get => GetValue(InlinesProperty);
- set => SetValue(InlinesProperty, value);
- }
-
///
/// Property for determining if the Copy command can be executed.
///
@@ -154,20 +124,12 @@ namespace Avalonia.Controls
private set => SetAndRaise(CanCopyProperty, ref _canCopy, value);
}
- public event EventHandler? CopyingToClipboard
- {
- add => AddHandler(CopyingToClipboardEvent, value);
- remove => RemoveHandler(CopyingToClipboardEvent, value);
- }
-
- internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
-
///
/// Copies the current selection to the Clipboard.
///
public async void Copy()
{
- if (_canCopy || !IsTextSelectionEnabled)
+ if (!_canCopy)
{
return;
}
@@ -188,45 +150,13 @@ namespace Avalonia.Controls
await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
.SetTextAsync(text);
}
- }
-
- protected override void RenderTextLayout(DrawingContext context, Point origin)
- {
- var selectionStart = SelectionStart;
- var selectionEnd = SelectionEnd;
- var selectionBrush = SelectionBrush;
-
- var selectionEnabled = IsTextSelectionEnabled;
-
- if (selectionEnabled && selectionStart != selectionEnd && selectionBrush != null)
- {
- var start = Math.Min(selectionStart, selectionEnd);
- var length = Math.Max(selectionStart, selectionEnd) - start;
-
- var rects = TextLayout.HitTestTextRange(start, length);
-
- using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
- {
- foreach (var rect in rects)
- {
- context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
- }
- }
- }
-
- base.RenderTextLayout(context, origin);
- }
+ }
///
/// Select all text in the TextBox
///
public void SelectAll()
{
- if (!IsTextSelectionEnabled)
- {
- return;
- }
-
var text = Text;
SelectionStart = 0;
@@ -238,94 +168,52 @@ namespace Avalonia.Controls
///
public void ClearSelection()
{
- if (!IsTextSelectionEnabled)
- {
- return;
- }
-
SelectionEnd = SelectionStart;
}
- protected void AddText(string? text)
+ protected override void OnGotFocus(GotFocusEventArgs e)
{
- if (string.IsNullOrEmpty(text))
- {
- return;
- }
-
- if (!HasComplexContent && string.IsNullOrEmpty(_text))
- {
- _text = text;
- }
- else
- {
- if (!string.IsNullOrEmpty(_text))
- {
- Inlines?.Add(_text);
-
- _text = null;
- }
-
- Inlines?.Add(text);
- }
- }
+ base.OnGotFocus(e);
- protected override string? GetText()
- {
- return _text ?? Inlines?.Text;
+ UpdateCommandStates();
}
- protected override void SetText(string? text)
+ protected override void OnLostFocus(RoutedEventArgs e)
{
- var oldValue = GetText();
+ base.OnLostFocus(e);
- AddText(text);
+ if ((ContextFlyout == null || !ContextFlyout.IsOpen) &&
+ (ContextMenu == null || !ContextMenu.IsOpen))
+ {
+ ClearSelection();
+ }
- RaisePropertyChanged(TextProperty, oldValue, text);
+ UpdateCommandStates();
}
- ///
- /// Creates the used to render the text.
- ///
- /// A object.
- protected override TextLayout CreateTextLayout(string? text)
+ protected override void RenderTextLayout(DrawingContext context, Point origin)
{
- var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
- var defaultProperties = new GenericTextRunProperties(
- typeface,
- FontSize,
- TextDecorations,
- Foreground);
-
- var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
- defaultProperties, TextWrapping, LineHeight, 0);
-
- ITextSource textSource;
+ var selectionStart = SelectionStart;
+ var selectionEnd = SelectionEnd;
+ var selectionBrush = SelectionBrush;
- if (_textRuns != null)
+ if (selectionStart != selectionEnd && selectionBrush != null)
{
- textSource = new InlinesTextSource(_textRuns);
- }
- else
- {
- textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
- }
+ var start = Math.Min(selectionStart, selectionEnd);
+ var length = Math.Max(selectionStart, selectionEnd) - start;
- return new TextLayout(
- textSource,
- paragraphProperties,
- TextTrimming,
- _constraint.Width,
- _constraint.Height,
- maxLines: MaxLines,
- lineHeight: LineHeight);
- }
+ var rects = TextLayout.HitTestTextRange(start, length);
- protected override void OnLostFocus(RoutedEventArgs e)
- {
- base.OnLostFocus(e);
+ using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
+ {
+ foreach (var rect in rects)
+ {
+ context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
+ }
+ }
+ }
- ClearSelection();
+ base.RenderTextLayout(context, origin);
}
protected override void OnKeyDown(KeyEventArgs e)
@@ -352,11 +240,6 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
- if (!IsTextSelectionEnabled)
- {
- return;
- }
-
var text = Text;
var clickInfo = e.GetCurrentPoint(this);
@@ -435,11 +318,6 @@ namespace Avalonia.Controls
{
base.OnPointerMoved(e);
- if (!IsTextSelectionEnabled)
- {
- return;
- }
-
// selection should not change during pointer move if the user right clicks
if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
@@ -486,11 +364,6 @@ namespace Avalonia.Controls
{
base.OnPointerReleased(e);
- if (!IsTextSelectionEnabled)
- {
- return;
- }
-
if (e.Pointer.Captured != this)
{
return;
@@ -521,100 +394,15 @@ namespace Avalonia.Controls
e.Pointer.Capture(null);
}
- protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
- {
- base.OnPropertyChanged(change);
-
- switch (change.Property.Name)
- {
- case nameof(Inlines):
- {
- OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
- InvalidateTextLayout();
- break;
- }
- }
- }
-
- protected override Size MeasureOverride(Size availableSize)
+ private void UpdateCommandStates()
{
- if(_textRuns != null)
- {
- LogicalChildren.Clear();
-
- VisualChildren.Clear();
-
- _textRuns = null;
- }
-
- if (Inlines != null && Inlines.Count > 0)
- {
- var inlines = Inlines;
-
- var textRuns = new List();
-
- foreach (var inline in inlines)
- {
- inline.BuildTextRun(textRuns);
- }
-
- foreach (var textRun in textRuns)
- {
- if (textRun is EmbeddedControlRun controlRun &&
- controlRun.Control is Control control)
- {
- LogicalChildren.Add(control);
-
- VisualChildren.Add(control);
-
- control.Measure(Size.Infinity);
- }
- }
-
- _textRuns = textRuns;
- }
-
- return base.MeasureOverride(availableSize);
- }
-
- protected override Size ArrangeOverride(Size finalSize)
- {
- if (HasComplexContent)
- {
- var currentY = 0.0;
-
- foreach (var textLine in TextLayout.TextLines)
- {
- var currentX = textLine.Start;
-
- foreach (var run in textLine.TextRuns)
- {
- if (run is DrawableTextRun drawable)
- {
- if (drawable is EmbeddedControlRun controlRun
- && controlRun.Control is Control control)
- {
- control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
- }
-
- currentX += drawable.Size.Width;
- }
- }
+ var text = GetSelection();
- currentY += textLine.Height;
- }
- }
-
- return base.ArrangeOverride(finalSize);
+ CanCopy = !string.IsNullOrEmpty(text);
}
private string GetSelection()
{
- if (!IsTextSelectionEnabled)
- {
- return "";
- }
-
var text = GetText();
if (string.IsNullOrEmpty(text))
@@ -638,59 +426,5 @@ namespace Avalonia.Controls
return selectedText;
}
-
- private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
- {
- if (oldValue is not null)
- {
- oldValue.Parent = null;
- oldValue.InlineHost = null;
- oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
- }
-
- if (newValue is not null)
- {
- newValue.Parent = this;
- newValue.InlineHost = this;
- newValue.Invalidated += (s, e) => InvalidateTextLayout();
- }
- }
-
- void IInlineHost.Invalidate()
- {
- InvalidateTextLayout();
- }
-
- private readonly struct InlinesTextSource : ITextSource
- {
- private readonly IReadOnlyList _textRuns;
-
- public InlinesTextSource(IReadOnlyList textRuns)
- {
- _textRuns = textRuns;
- }
-
- public TextRun? GetTextRun(int textSourceIndex)
- {
- var currentPosition = 0;
-
- foreach (var textRun in _textRuns)
- {
- if (textRun.TextSourceLength == 0)
- {
- continue;
- }
-
- if (currentPosition >= textSourceIndex)
- {
- return textRun;
- }
-
- currentPosition += textRun.TextSourceLength;
- }
-
- return null;
- }
- }
}
}
diff --git a/src/Avalonia.Controls/SizeChangedEventArgs.cs b/src/Avalonia.Controls/SizeChangedEventArgs.cs
new file mode 100644
index 0000000000..2dc642b163
--- /dev/null
+++ b/src/Avalonia.Controls/SizeChangedEventArgs.cs
@@ -0,0 +1,83 @@
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data specific to a SizeChanged event.
+ ///
+ public class SizeChangedEventArgs : RoutedEventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The routed event associated with these event args.
+ public SizeChangedEventArgs(RoutedEvent? routedEvent)
+ : base (routedEvent)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The routed event associated with these event args.
+ /// The source object that raised the routed event.
+ public SizeChangedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
+ : base(routedEvent, source)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The routed event associated with these event args.
+ /// The source object that raised the routed event.
+ /// The previous size (or bounds) of the object.
+ /// The new size (or bounds) of the object.
+ public SizeChangedEventArgs(
+ RoutedEvent? routedEvent,
+ IInteractive? source,
+ Size previousSize,
+ Size newSize)
+ : base(routedEvent, source)
+ {
+ PreviousSize = previousSize;
+ NewSize = newSize;
+ }
+
+ ///
+ /// Gets a value indicating whether the height of the new size is considered
+ /// different than the previous size height.
+ ///
+ ///
+ /// This will take into account layout epsilon and will not be true if both
+ /// heights are considered equivalent for layout purposes. Remember there can
+ /// be small variations in the calculations between layout cycles due to
+ /// rounding and precision even when the size has not otherwise changed.
+ ///
+ public bool HeightChanged => !MathUtilities.AreClose(NewSize.Height, PreviousSize.Height, LayoutHelper.LayoutEpsilon);
+
+ ///
+ /// Gets the new size (or bounds) of the object.
+ ///
+ public Size NewSize { get; init; }
+
+ ///
+ /// Gets the previous size (or bounds) of the object.
+ ///
+ public Size PreviousSize { get; init; }
+
+ ///
+ /// Gets a value indicating whether the width of the new size is considered
+ /// different than the previous size width.
+ ///
+ ///
+ /// This will take into account layout epsilon and will not be true if both
+ /// widths are considered equivalent for layout purposes. Remember there can
+ /// be small variations in the calculations between layout cycles due to
+ /// rounding and precision even when the size has not otherwise changed.
+ ///
+ public bool WidthChanged => !MathUtilities.AreClose(NewSize.Width, PreviousSize.Width, LayoutHelper.LayoutEpsilon);
+ }
+}
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 99c8068b3d..c8e05e5cb3 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents;
using Avalonia.Layout;
@@ -12,7 +13,7 @@ namespace Avalonia.Controls
///
/// A control that displays a block of text.
///
- public class TextBlock : Control, IAddChild
+ public class TextBlock : Control, IInlineHost
{
///
/// Defines the property.
@@ -81,6 +82,15 @@ namespace Avalonia.Controls
validate: IsValidLineHeight,
inherits: true);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AttachedProperty LetterSpacingProperty =
+ AvaloniaProperty.RegisterAttached(
+ nameof(LetterSpacing),
+ 0,
+ inherits: true);
+
///
/// Defines the property.
///
@@ -96,15 +106,15 @@ namespace Avalonia.Controls
public static readonly DirectProperty TextProperty =
AvaloniaProperty.RegisterDirect(
nameof(Text),
- o => o.Text,
- (o, v) => o.Text = v);
+ o => o.GetText(),
+ (o, v) => o.SetText(v));
///
/// Defines the property.
///
public static readonly AttachedProperty TextAlignmentProperty =
AvaloniaProperty.RegisterAttached(
- nameof(TextAlignment),
+ nameof(TextAlignment),
defaultValue: TextAlignment.Start,
inherits: true);
@@ -112,14 +122,14 @@ namespace Avalonia.Controls
/// Defines the property.
///
public static readonly AttachedProperty TextWrappingProperty =
- AvaloniaProperty.RegisterAttached(nameof(TextWrapping),
+ AvaloniaProperty.RegisterAttached(nameof(TextWrapping),
inherits: true);
///
/// Defines the property.
///
public static readonly AttachedProperty TextTrimmingProperty =
- AvaloniaProperty.RegisterAttached(nameof(TextTrimming),
+ AvaloniaProperty.RegisterAttached(nameof(TextTrimming),
defaultValue: TextTrimming.None,
inherits: true);
@@ -129,9 +139,17 @@ namespace Avalonia.Controls
public static readonly StyledProperty TextDecorationsProperty =
AvaloniaProperty.Register(nameof(TextDecorations));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty InlinesProperty =
+ AvaloniaProperty.Register(
+ nameof(Inlines));
+
internal string? _text;
protected TextLayout? _textLayout;
protected Size _constraint;
+ private IReadOnlyList? _textRuns;
///
/// Initializes static members of the class.
@@ -139,10 +157,19 @@ namespace Avalonia.Controls
static TextBlock()
{
ClipToBoundsProperty.OverrideDefaultValue(true);
-
+
AffectsRender(BackgroundProperty, ForegroundProperty);
}
+ public TextBlock()
+ {
+ Inlines = new InlineCollection
+ {
+ LogicalChildren = LogicalChildren,
+ InlineHost = this
+ };
+ }
+
///
/// Gets the used to render the text.
///
@@ -244,6 +271,15 @@ namespace Avalonia.Controls
set => SetValue(LineHeightProperty, value);
}
+ ///
+ /// Gets or sets the letter spacing.
+ ///
+ public double LetterSpacing
+ {
+ get => GetValue(LetterSpacingProperty);
+ set => SetValue(LetterSpacingProperty, value);
+ }
+
///
/// Gets or sets the maximum number of text lines.
///
@@ -288,9 +324,21 @@ namespace Avalonia.Controls
get => GetValue(TextDecorationsProperty);
set => SetValue(TextDecorationsProperty, value);
}
-
+
+ ///
+ /// Gets or sets the inlines.
+ ///
+ [Content]
+ public InlineCollection? Inlines
+ {
+ get => GetValue(InlinesProperty);
+ set => SetValue(InlinesProperty, value);
+ }
+
protected override bool BypassFlowDirectionPolicies => true;
+ internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
+
///
/// The BaselineOffset property provides an adjustment to baseline offset
///
@@ -445,6 +493,35 @@ namespace Avalonia.Controls
control.SetValue(LineHeightProperty, height);
}
+ ///
+ /// Reads the attached property from the given element
+ ///
+ /// The element to which to read the attached property.
+ public static double GetLetterSpacing(Control control)
+ {
+ if (control == null)
+ {
+ throw new ArgumentNullException(nameof(control));
+ }
+
+ return control.GetValue(LetterSpacingProperty);
+ }
+
+ ///
+ /// Writes the attached property LetterSpacing to the given element.
+ ///
+ /// The element to which to write the attached property.
+ /// The property value to set
+ public static void SetLetterSpacing(Control control, double letterSpacing)
+ {
+ if (control == null)
+ {
+ throw new ArgumentNullException(nameof(control));
+ }
+
+ control.SetValue(LetterSpacingProperty, letterSpacing);
+ }
+
///
/// Reads the attached property from the given element
///
@@ -513,19 +590,19 @@ namespace Avalonia.Controls
TextLayout.Draw(context, origin);
}
- void IAddChild.AddChild(string text)
- {
- _text = text;
- }
-
protected virtual string? GetText()
{
- return _text;
+ return _text ?? Inlines?.Text;
}
protected virtual void SetText(string? text)
{
- SetAndRaise(TextProperty, ref _text, text);
+ if (HasComplexContent)
+ {
+ Inlines?.Clear();
+ }
+
+ SetAndRaise(TextProperty, ref _text, text);
}
///
@@ -534,17 +611,30 @@ namespace Avalonia.Controls
/// A object.
protected virtual TextLayout CreateTextLayout(string? text)
{
+ var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
+
var defaultProperties = new GenericTextRunProperties(
- new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
+ typeface,
FontSize,
TextDecorations,
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
- defaultProperties, TextWrapping, LineHeight, 0);
+ defaultProperties, TextWrapping, LineHeight, 0, LetterSpacing);
+
+ ITextSource textSource;
+
+ if (_textRuns != null)
+ {
+ textSource = new InlinesTextSource(_textRuns);
+ }
+ else
+ {
+ textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
+ }
return new TextLayout(
- new SimpleTextSource((text ?? "").AsMemory(), defaultProperties),
+ textSource,
paragraphProperties,
TextTrimming,
_constraint.Width,
@@ -560,6 +650,8 @@ namespace Avalonia.Controls
{
_textLayout = null;
+ InvalidateVisual();
+
InvalidateMeasure();
}
@@ -573,7 +665,46 @@ namespace Avalonia.Controls
_textLayout = null;
- InvalidateArrange();
+ var inlines = Inlines;
+
+ if (HasComplexContent)
+ {
+ if (_textRuns != null)
+ {
+ foreach (var textRun in _textRuns)
+ {
+ if (textRun is EmbeddedControlRun controlRun &&
+ controlRun.Control is Control control)
+ {
+ VisualChildren.Remove(control);
+
+ LogicalChildren.Remove(control);
+ }
+ }
+ }
+
+ var textRuns = new List();
+
+ foreach (var inline in inlines!)
+ {
+ inline.BuildTextRun(textRuns);
+ }
+
+ foreach (var textRun in textRuns)
+ {
+ if (textRun is EmbeddedControlRun controlRun &&
+ controlRun.Control is Control control)
+ {
+ VisualChildren.Add(control);
+
+ LogicalChildren.Add(control);
+
+ control.Measure(Size.Infinity);
+ }
+ }
+
+ _textRuns = textRuns;
+ }
var measuredSize = TextLayout.Bounds.Size.Inflate(padding);
@@ -584,16 +715,11 @@ namespace Avalonia.Controls
{
var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
- if(finalSize.Width < textWidth)
+ if (finalSize.Width < textWidth)
{
finalSize = finalSize.WithWidth(textWidth);
}
- if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
- {
- return finalSize;
- }
-
var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
@@ -602,6 +728,32 @@ namespace Avalonia.Controls
_textLayout = null;
+ if (HasComplexContent)
+ {
+ var currentY = padding.Top;
+
+ foreach (var textLine in TextLayout.TextLines)
+ {
+ var currentX = padding.Left + textLine.Start;
+
+ foreach (var run in textLine.TextRuns)
+ {
+ if (run is DrawableTextRun drawable)
+ {
+ if (drawable is EmbeddedControlRun controlRun
+ && controlRun.Control is Control control)
+ {
+ control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
+ }
+
+ currentX += drawable.Size.Width;
+ }
+ }
+
+ currentY += textLine.Height;
+ }
+ }
+
return finalSize;
}
@@ -610,42 +762,71 @@ namespace Avalonia.Controls
return new TextBlockAutomationPeer(this);
}
- private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
-
- private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
-
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
- case nameof (FontSize):
- case nameof (FontWeight):
- case nameof (FontStyle):
- case nameof (FontFamily):
- case nameof (FontStretch):
+ case nameof(FontSize):
+ case nameof(FontWeight):
+ case nameof(FontStyle):
+ case nameof(FontFamily):
+ case nameof(FontStretch):
- case nameof (TextWrapping):
- case nameof (TextTrimming):
- case nameof (TextAlignment):
+ case nameof(TextWrapping):
+ case nameof(TextTrimming):
+ case nameof(TextAlignment):
- case nameof (FlowDirection):
+ case nameof(FlowDirection):
case nameof (Padding):
case nameof (LineHeight):
+ case nameof (LetterSpacing):
case nameof (MaxLines):
- case nameof (Text):
- case nameof (TextDecorations):
- case nameof (Foreground):
- {
- InvalidateTextLayout();
- break;
- }
+ case nameof(Text):
+ case nameof(TextDecorations):
+ case nameof(Foreground):
+ {
+ InvalidateTextLayout();
+ break;
+ }
+ case nameof(Inlines):
+ {
+ OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
+ InvalidateTextLayout();
+ break;
+ }
+ }
+ }
+
+ private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
+
+ private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
+
+ private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
+ {
+ if (oldValue is not null)
+ {
+ oldValue.LogicalChildren = null;
+ oldValue.InlineHost = null;
+ oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
+ }
+
+ if (newValue is not null)
+ {
+ newValue.LogicalChildren = LogicalChildren;
+ newValue.InlineHost = this;
+ newValue.Invalidated += (s, e) => InvalidateTextLayout();
}
}
+ void IInlineHost.Invalidate()
+ {
+ InvalidateTextLayout();
+ }
+
protected readonly struct SimpleTextSource : ITextSource
{
private readonly ReadOnlySlice _text;
@@ -674,5 +855,46 @@ namespace Avalonia.Controls
return new TextCharacters(runText, _defaultProperties);
}
}
+
+ private readonly struct InlinesTextSource : ITextSource
+ {
+ private readonly IReadOnlyList _textRuns;
+
+ public InlinesTextSource(IReadOnlyList textRuns)
+ {
+ _textRuns = textRuns;
+ }
+
+ public IReadOnlyList TextRuns => _textRuns;
+
+ public TextRun? GetTextRun(int textSourceIndex)
+ {
+ var currentPosition = 0;
+
+ foreach (var textRun in _textRuns)
+ {
+ if (textRun.TextSourceLength == 0)
+ {
+ continue;
+ }
+
+ if (textSourceIndex >= currentPosition + textRun.TextSourceLength)
+ {
+ currentPosition += textRun.TextSourceLength;
+
+ continue;
+ }
+
+ if (textRun is TextCharacters)
+ {
+ return new TextCharacters(textRun.Text.Skip(Math.Max(0, textSourceIndex - currentPosition)), textRun.Properties!);
+ }
+
+ return textRun;
+ }
+
+ return null;
+ }
+ }
}
}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index da4e90fb66..d5b45398e7 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -114,6 +114,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner();
+ ///
+ /// Defines see property.
+ ///
+ public static readonly StyledProperty LetterSpacingProperty =
+ TextBlock.LetterSpacingProperty.AddOwner();
+
public static readonly StyledProperty WatermarkProperty =
AvaloniaProperty.Register(nameof(Watermark));
@@ -378,6 +384,12 @@ namespace Avalonia.Controls
set => SetValue(MaxLinesProperty, value);
}
+ public double LetterSpacing
+ {
+ get => GetValue(LetterSpacingProperty);
+ set => SetValue(LetterSpacingProperty, value);
+ }
+
///
/// Gets or sets the line height.
///
@@ -1200,7 +1212,7 @@ namespace Avalonia.Controls
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
- if (_presenter == null || !string.IsNullOrEmpty(_presenter.PreeditText))
+ if (_presenter == null )
{
return;
}
diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
index 5277435858..cd14b2d69a 100644
--- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
+++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
@@ -9,7 +9,7 @@ using Avalonia.Rendering;
namespace Avalonia.DesignerSupport.Remote
{
- class PreviewerWindowingPlatform : IWindowingPlatform, IPlatformSettings
+ class PreviewerWindowingPlatform : IWindowingPlatform
{
static readonly IKeyboardDevice Keyboard = new KeyboardDevice();
private static IAvaloniaRemoteTransportConnection s_transport;
@@ -51,7 +51,7 @@ namespace Avalonia.DesignerSupport.Remote
.Bind().ToSingleton()
.Bind().ToSingleton()
.Bind().ToConstant(Keyboard)
- .Bind().ToConstant(instance)
+ .Bind().ToSingleton()
.Bind().ToConstant(threading)
.Bind().ToConstant(new RenderLoop())
.Bind().ToConstant(new DefaultRenderTimer(60))
@@ -60,12 +60,5 @@ namespace Avalonia.DesignerSupport.Remote
.Bind().ToSingleton();
}
-
- public Size DoubleClickSize { get; } = new Size(2, 2);
- public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500);
-
- public Size TouchDoubleClickSize => new Size(16, 16);
-
- public TimeSpan TouchDoubleClickTime => DoubleClickTime;
}
}
diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
index 4fcec6fd52..8da10fa59b 100644
--- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
+++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
@@ -63,7 +63,7 @@ namespace Avalonia.Headless
.Bind().ToConstant(new HeadlessPlatformThreadingInterface())
.Bind().ToSingleton()
.Bind().ToSingleton()
- .Bind().ToConstant(new HeadlessPlatformSettingsStub())
+ .Bind().ToSingleton()
.Bind().ToSingleton()
.Bind().ToConstant(new KeyboardDevice())
.Bind().ToConstant(new RenderLoop())
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index fcd7f1e31f..7abc0ca131 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -115,19 +115,16 @@ namespace Avalonia.Headless
return new HeadlessGeometryStub(new Rect(glyphRun.Size));
}
- public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets)
{
- return new HeadlessGlyphRunBufferStub();
+ return new HeadlessGlyphRunStub();
}
- public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- return new HeadlessHorizontalGlyphRunBufferStub();
- }
-
- public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
+ class HeadlessGlyphRunStub : IGlyphRunImpl
{
- return new HeadlessPositionedGlyphRunBufferStub();
+ public void Dispose()
+ {
+ }
}
class HeadlessGeometryStub : IGeometryImpl
@@ -213,33 +210,6 @@ namespace Avalonia.Headless
public Matrix Transform { get; }
}
- class HeadlessGlyphRunBufferStub : IGlyphRunBuffer
- {
- public Span GlyphIndices => Span.Empty;
-
- public IGlyphRunImpl Build()
- {
- return new HeadlessGlyphRunStub();
- }
- }
-
- class HeadlessHorizontalGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IHorizontalGlyphRunBuffer
- {
- public Span GlyphPositions => Span.Empty;
- }
-
- class HeadlessPositionedGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IPositionedGlyphRunBuffer
- {
- public Span GlyphPositions => Span.Empty;
- }
-
- class HeadlessGlyphRunStub : IGlyphRunImpl
- {
- public void Dispose()
- {
- }
- }
-
class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl
{
public HeadlessStreamingGeometryStub() : base(Rect.Empty)
@@ -353,12 +323,12 @@ namespace Avalonia.Headless
public Vector Dpi { get; }
public PixelSize PixelSize { get; }
public int Version { get; set; }
- public void Save(string fileName)
+ public void Save(string fileName, int? quality = null)
{
}
- public void Save(Stream stream)
+ public void Save(Stream stream, int? quality = null)
{
}
diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
index 76948e9286..688b8e0398 100644
--- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
@@ -65,16 +65,6 @@ namespace Avalonia.Headless
}
}
- class HeadlessPlatformSettingsStub : IPlatformSettings
- {
- public Size DoubleClickSize { get; } = new Size(2, 2);
- public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500);
-
- public Size TouchDoubleClickSize => new Size(16,16);
-
- public TimeSpan TouchDoubleClickTime => DoubleClickTime;
- }
-
class HeadlessGlyphTypefaceImpl : IGlyphTypeface
{
public FontMetrics Metrics => new FontMetrics
diff --git a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj
index e784bda105..14dafb7284 100644
--- a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj
+++ b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj
@@ -4,7 +4,7 @@
true
-
+
false
all
diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj
index 4ceb1be340..c924ef3a45 100644
--- a/src/Avalonia.Native/Avalonia.Native.csproj
+++ b/src/Avalonia.Native/Avalonia.Native.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs
index 532893884a..db6b6f57cd 100644
--- a/src/Avalonia.Native/AvaloniaNativePlatform.cs
+++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs
@@ -14,7 +14,7 @@ using MicroCom.Runtime;
namespace Avalonia.Native
{
- class AvaloniaNativePlatform : IPlatformSettings, IWindowingPlatform
+ class AvaloniaNativePlatform : IWindowingPlatform
{
private readonly IAvaloniaNativeFactory _factory;
private AvaloniaNativePlatformOptions _options;
@@ -26,16 +26,6 @@ namespace Avalonia.Native
internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice();
[CanBeNull] internal static Compositor Compositor { get; private set; }
- public Size DoubleClickSize => new Size(4, 4);
-
- public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); //TODO
-
- ///
- public Size TouchDoubleClickSize => new Size(16, 16);
-
- ///
- public TimeSpan TouchDoubleClickTime => DoubleClickTime;
-
public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options)
{
var result = new AvaloniaNativePlatform(MicroComRuntime.CreateProxyFor(factory, true));
@@ -103,6 +93,8 @@ namespace Avalonia.Native
var macOpts = AvaloniaLocator.Current.GetService() ?? new MacOSPlatformOptions();
_factory.MacOptions.SetShowInDock(macOpts.ShowInDock ? 1 : 0);
+ _factory.MacOptions.SetDisableSetProcessName(macOpts.DisableSetProcessName ? 1 : 0);
+ _factory.MacOptions.SetDisableAppDelegate(macOpts.DisableAvaloniaAppDelegate ? 1 : 0);
}
AvaloniaLocator.CurrentMutable
@@ -111,7 +103,7 @@ namespace Avalonia.Native
.Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
.Bind().ToSingleton()
.Bind().ToConstant(KeyboardDevice)
- .Bind().ToConstant(this)
+ .Bind().ToSingleton()
.Bind().ToConstant(this)
.Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))
.Bind().ToConstant(new DefaultRenderTimer(60))
diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
index 61889aa9e4..189f45d7c8 100644
--- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
+++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
@@ -83,5 +83,13 @@ namespace Avalonia
/// Gets or sets a value indicating whether the native macOS menu bar will be enabled for the application.
///
public bool DisableNativeMenus { get; set; }
+
+ public bool DisableSetProcessName { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether Avalonia can install its own AppDelegate.
+ /// Disabling this can be useful in some scenarios like when running as a plugin inside an existing macOS application.
+ ///
+ public bool DisableAvaloniaAppDelegate { get; set; }
}
}
diff --git a/src/Avalonia.Native/DoubleClickHelper.cs b/src/Avalonia.Native/DoubleClickHelper.cs
index 7618d6976a..02e74d7d63 100644
--- a/src/Avalonia.Native/DoubleClickHelper.cs
+++ b/src/Avalonia.Native/DoubleClickHelper.cs
@@ -1,3 +1,4 @@
+using Avalonia.Input;
using Avalonia.Platform;
namespace Avalonia.Native
@@ -13,7 +14,8 @@ namespace Avalonia.Native
Point p)
{
var settings = AvaloniaLocator.Current.GetService();
- var doubleClickTime = settings.DoubleClickTime.TotalMilliseconds;
+ var doubleClickTime = settings?.GetDoubleTapTime(PointerType.Mouse).TotalMilliseconds ?? 500;
+ var doubleClickSize = settings?.GetDoubleTapSize(PointerType.Mouse) ?? new Size(4, 4);
if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime)
{
@@ -23,7 +25,7 @@ namespace Avalonia.Native
++_clickCount;
_lastClickTime = timestamp;
_lastClickRect = new Rect(p, new Size())
- .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2));
+ .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2));
return _clickCount == 2;
}
diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl
index b0bff07146..b90e144eab 100644
--- a/src/Avalonia.Native/avn.idl
+++ b/src/Avalonia.Native/avn.idl
@@ -599,6 +599,8 @@ interface IAvnMacOptions : IUnknown
{
HRESULT SetShowInDock(int show);
HRESULT SetApplicationTitle(char* utf8string);
+ HRESULT SetDisableSetProcessName(int disable);
+ HRESULT SetDisableAppDelegate(int disable);
}
[uuid(04c1b049-1f43-418a-9159-cae627ec1367)]
diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
index 577539b26b..5383aa3180 100644
--- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
@@ -68,7 +68,7 @@
-
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml
deleted file mode 100644
index 75af2efcb1..0000000000
--- a/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml b/src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml
new file mode 100644
index 0000000000..f630969ae6
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
index 17c69da8fd..224264b51d 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
@@ -161,6 +161,7 @@
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}"
LineHeight="{TemplateBinding LineHeight}"
+ LetterSpacing="{TemplateBinding LetterSpacing}"
PasswordChar="{TemplateBinding PasswordChar}"
RevealPassword="{TemplateBinding RevealPassword}"
SelectionBrush="{TemplateBinding SelectionBrush}"
@@ -234,7 +235,8 @@
+ IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}"
+ ClipToBounds="True">
+ Command="{Binding $parent[TextBox].Clear}"
+ ClipToBounds="True">
diff --git a/src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml
deleted file mode 100644
index c0570282cb..0000000000
--- a/src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml b/src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml
new file mode 100644
index 0000000000..aaa6448aea
--- /dev/null
+++ b/src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
index 644c6ed416..4aefa0136c 100644
--- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
@@ -64,7 +64,7 @@
-
+
diff --git a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
index 5fa6412688..8428e3aae7 100644
--- a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
@@ -149,14 +149,14 @@
CaretBrush="{TemplateBinding CaretBrush}"
CaretIndex="{TemplateBinding CaretIndex}"
LineHeight="{TemplateBinding LineHeight}"
+ LetterSpacing="{TemplateBinding LetterSpacing}"
PasswordChar="{TemplateBinding PasswordChar}"
RevealPassword="{TemplateBinding RevealPassword}"
SelectionBrush="{TemplateBinding SelectionBrush}"
SelectionEnd="{TemplateBinding SelectionEnd}"
SelectionForegroundBrush="{TemplateBinding SelectionForegroundBrush}"
SelectionStart="{TemplateBinding SelectionStart}"
- Text="{TemplateBinding Text,
- Mode=TwoWay}"
+ Text="{TemplateBinding Text,Mode=TwoWay}"
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}" />
@@ -192,7 +192,8 @@
+ Theme="{StaticResource SimpleTextBoxClearButtonTheme}"
+ ClipToBounds="True" />
@@ -212,7 +213,8 @@
+ Theme="{StaticResource SimplePasswordBoxRevealButtonTheme}"
+ ClipToBounds="True" />
diff --git a/src/Avalonia.X11/Stubs.cs b/src/Avalonia.X11/Stubs.cs
deleted file mode 100644
index f73512f1e8..0000000000
--- a/src/Avalonia.X11/Stubs.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System;
-using Avalonia.Platform;
-
-namespace Avalonia.X11
-{
- class PlatformSettingsStub : IPlatformSettings
- {
- public Size DoubleClickSize { get; } = new Size(2, 2);
- public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500);
-
- ///
- public Size TouchDoubleClickSize => new Size(16, 16);
-
- ///
- public TimeSpan TouchDoubleClickTime => DoubleClickTime;
- }
-}
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index 381cdd74c3..96dc16e186 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -80,7 +80,7 @@ namespace Avalonia.X11
.Bind().ToFunc(() => KeyboardDevice)
.Bind().ToConstant(new X11CursorFactory(Display))
.Bind().ToConstant(new X11Clipboard(this))
- .Bind().ToConstant(new PlatformSettingsStub())
+ .Bind().ToSingleton()
.Bind().ToConstant(new X11IconLoader())
.Bind().ToConstant(new LinuxMountedVolumeInfoProvider())
.Bind().ToConstant(new X11PlatformLifetimeEvents(this));
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs b/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs
index e92ad02c7a..ce843952e7 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs
@@ -23,5 +23,11 @@ namespace Avalonia.LinuxFramebuffer
/// Default: R0 G0 B0 A0
///
public Color InitialBufferSwappingColor { get; set; } = new Color(0, 0, 0, 0);
+
+ ///
+ /// specific the video mode with which the DrmOutput should be created, if it is not found it will fallback to the preferred mode.
+ /// If NULL preferred mode will be used.
+ ///
+ public PixelSize? VideoMode { get; set; }
}
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.Pointer.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.Pointer.cs
new file mode 100644
index 0000000000..ef039a38bc
--- /dev/null
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.Pointer.cs
@@ -0,0 +1,110 @@
+using System;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Logging;
+using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods;
+
+namespace Avalonia.LinuxFramebuffer.Input.LibInput;
+
+public partial class LibInputBackend
+{
+ private MouseDevice _mouse = new MouseDevice();
+ private Point _mousePosition;
+ private const string Pointer = LibInput + "/" + nameof(Pointer);
+
+ private void HandlePointer(IntPtr ev, LibInputEventType type)
+ {
+ var modifiers = RawInputModifiers.None; //TODO: support input modifiers
+ var pev = libinput_event_get_pointer_event(ev);
+ var info = _screen.ScaledSize;
+ var ts = libinput_event_pointer_get_time_usec(pev) / 1000;
+ switch (type)
+ {
+ case LibInputEventType.LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE:
+ _mousePosition = new Point(libinput_event_pointer_get_absolute_x_transformed(pev, (int)info.Width),
+ libinput_event_pointer_get_absolute_y_transformed(pev, (int)info.Height));
+ ScheduleInput(new RawPointerEventArgs(_mouse, ts, _inputRoot, RawPointerEventType.Move, _mousePosition,
+ modifiers));
+ break;
+ case LibInputEventType.LIBINPUT_EVENT_POINTER_BUTTON:
+ {
+ var button = (EvKey)libinput_event_pointer_get_button(pev);
+ var buttonState = libinput_event_pointer_get_button_state(pev);
+
+ RawPointerEventArgs evnt = button switch
+ {
+ EvKey.BTN_LEFT when buttonState == 1
+ => new(_mouse, ts, _inputRoot, RawPointerEventType.LeftButtonDown, _mousePosition, modifiers),
+ EvKey.BTN_LEFT when buttonState == 0
+ => new(_mouse, ts, _inputRoot, RawPointerEventType.LeftButtonUp, _mousePosition, modifiers),
+ EvKey.BTN_RIGHT when buttonState == 1
+ => new(_mouse, ts, _inputRoot, RawPointerEventType.RightButtonUp, _mousePosition, modifiers),
+ EvKey.BTN_RIGHT when buttonState == 2
+ => new(_mouse, ts, _inputRoot, RawPointerEventType.RightButtonDown, _mousePosition, modifiers),
+ EvKey.BTN_MIDDLE when buttonState == 1
+ => new(_mouse, ts, _inputRoot, RawPointerEventType.MiddleButtonDown, _mousePosition, modifiers),
+ EvKey.BTN_MIDDLE when buttonState == 2
+ => new(_mouse, ts, _inputRoot, RawPointerEventType.MiddleButtonUp, _mousePosition, modifiers),
+ _ => default,
+ };
+ if (evnt is not null)
+ {
+ ScheduleInput(evnt);
+ }
+ else
+ {
+ Logger.TryGet(LogEventLevel.Warning, Pointer)
+ ?.Log(this, $"The button {button} is not associated");
+ }
+ }
+ break;
+ // Backward compatibility with low-res wheel
+ case LibInputEventType.LIBINPUT_EVENT_POINTER_AXIS:
+ {
+ var sourceAxis = libinput_event_pointer_get_axis_source(pev);
+ switch (sourceAxis)
+ {
+ case LibInputPointerAxisSource.LIBINPUT_POINTER_AXIS_SOURCE_WHEEL:
+ {
+ var value = libinput_event_pointer_get_axis_value_discrete(pev,
+ LibInputPointerAxis.LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL);
+ ScheduleInput(new RawMouseWheelEventArgs(_mouse
+ , ts
+ , _inputRoot
+ , _mousePosition
+ , new Vector(0, -value)
+ , modifiers));
+ }
+ break;
+ case LibInputPointerAxisSource.LIBINPUT_POINTER_AXIS_SOURCE_FINGER:
+ case LibInputPointerAxisSource.LIBINPUT_POINTER_AXIS_SOURCE_CONTINUOUS:
+ case LibInputPointerAxisSource.LIBINPUT_POINTER_AXIS_SOURCE_WHEEL_TILT:
+ default:
+ Logger.TryGet(LogEventLevel.Debug, Pointer)
+ ?.Log(this, $"The pointer axis {sourceAxis} is not managed.");
+ break;
+ }
+ }
+ break;
+ // Hi-Res wheel
+ case LibInputEventType.LIBINPUT_EVENT_POINTER_SCROLL_WHEEL:
+ {
+ var value = new Vector(0,
+ -libinput_event_pointer_get_scroll_value_v120(pev,
+ LibInputPointerAxis.LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL) / 120);
+ ScheduleInput(new RawMouseWheelEventArgs(_mouse
+ , ts
+ , _inputRoot
+ , _mousePosition
+ , value
+ , modifiers));
+ }
+ break;
+ default:
+ Logger.TryGet(LogEventLevel.Warning, Pointer)
+ ?.Log(this, $"The pointer event {type} is not mapped.");
+ break;
+ }
+
+ }
+}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
index 702ae3f8e5..77e8202fac 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
@@ -4,18 +4,16 @@ using System.IO;
using System.Threading;
using Avalonia.Input;
using Avalonia.Input.Raw;
-using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods;
+using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods;
namespace Avalonia.LinuxFramebuffer.Input.LibInput
{
- public class LibInputBackend : IInputBackend
+ public partial class LibInputBackend : IInputBackend
{
private IScreenInfoProvider _screen;
private IInputRoot _inputRoot;
private readonly Queue _inputThreadActions = new Queue();
private TouchDevice _touch = new TouchDevice();
- private MouseDevice _mouse = new MouseDevice();
- private Point _mousePosition;
-
+ private const string LibInput = nameof(Avalonia.LinuxFramebuffer) + "/" + nameof(Avalonia.LinuxFramebuffer.Input) + "/" + nameof(LibInput);
private readonly RawEventGroupingThreadingHelper _inputQueue;
private Action _onInput;
private Dictionary _pointers = new Dictionary();
@@ -24,13 +22,13 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
{
var ctx = libinput_path_create_context();
_inputQueue = new(e => _onInput?.Invoke(e));
- new Thread(()=>InputThread(ctx)).Start();
+ new Thread(() => InputThread(ctx)).Start();
}
private unsafe void InputThread(IntPtr ctx)
{
var fd = libinput_get_fd(ctx);
-
+
var timeval = stackalloc IntPtr[2];
@@ -38,12 +36,11 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
libinput_path_add_device(ctx, f);
while (true)
{
-
IntPtr ev;
libinput_dispatch(ctx);
while ((ev = libinput_get_event(ctx)) != IntPtr.Zero)
{
-
+
var type = libinput_event_get_type(ev);
if (type >= LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN &&
type <= LibInputEventType.LIBINPUT_EVENT_TOUCH_CANCEL)
@@ -52,12 +49,12 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
if (type >= LibInputEventType.LIBINPUT_EVENT_POINTER_MOTION
&& type <= LibInputEventType.LIBINPUT_EVENT_POINTER_AXIS)
HandlePointer(ev, type);
-
+
libinput_event_destroy(ev);
libinput_dispatch(ctx);
}
- pollfd pfd = new pollfd {fd = fd, events = 1};
+ pollfd pfd = new pollfd { fd = fd, events = 1 };
NativeUnsafeMethods.poll(&pfd, new IntPtr(1), 10);
}
}
@@ -67,7 +64,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
private void HandleTouch(IntPtr ev, LibInputEventType type)
{
var tev = libinput_event_get_touch_event(ev);
- if(tev == IntPtr.Zero)
+ if (tev == IntPtr.Zero)
return;
if (type < LibInputEventType.LIBINPUT_EVENT_TOUCH_FRAME)
{
@@ -102,44 +99,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
}
}
- private void HandlePointer(IntPtr ev, LibInputEventType type)
- {
- //TODO: support input modifiers
- var pev = libinput_event_get_pointer_event(ev);
- var info = _screen.ScaledSize;
- var ts = libinput_event_pointer_get_time_usec(pev) / 1000;
- if (type == LibInputEventType.LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE)
- {
- _mousePosition = new Point(libinput_event_pointer_get_absolute_x_transformed(pev, (int)info.Width),
- libinput_event_pointer_get_absolute_y_transformed(pev, (int)info.Height));
- ScheduleInput(new RawPointerEventArgs(_mouse, ts, _inputRoot, RawPointerEventType.Move, _mousePosition,
- RawInputModifiers.None));
- }
- else if (type == LibInputEventType.LIBINPUT_EVENT_POINTER_BUTTON)
- {
- var button = (EvKey)libinput_event_pointer_get_button(pev);
- var buttonState = libinput_event_pointer_get_button_state(pev);
-
-
- var evnt = button == EvKey.BTN_LEFT ?
- (buttonState == 1 ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp) :
- button == EvKey.BTN_MIDDLE ?
- (buttonState == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp) :
- button == EvKey.BTN_RIGHT ?
- (buttonState == 1 ?
- RawPointerEventType.RightButtonDown :
- RawPointerEventType.RightButtonUp) :
- (RawPointerEventType)(-1);
- if (evnt == (RawPointerEventType)(-1))
- return;
-
-
- ScheduleInput(
- new RawPointerEventArgs(_mouse, ts, _inputRoot, evnt, _mousePosition, RawInputModifiers.None));
- }
-
- }
-
public void Initialize(IScreenInfoProvider screen, Action onInput)
{
_screen = screen;
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs
index 0492090461..df6defb653 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs
@@ -77,6 +77,9 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE,
LIBINPUT_EVENT_POINTER_BUTTON,
LIBINPUT_EVENT_POINTER_AXIS,
+ LIBINPUT_EVENT_POINTER_SCROLL_WHEEL,
+ LIBINPUT_EVENT_POINTER_SCROLL_FINGER,
+ LIBINPUT_EVENT_POINTER_SCROLL_CONTINUOUS,
LIBINPUT_EVENT_TOUCH_DOWN = 500,
LIBINPUT_EVENT_TOUCH_UP,
LIBINPUT_EVENT_TOUCH_MOTION,
@@ -97,8 +100,38 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
LIBINPUT_EVENT_GESTURE_PINCH_END,
LIBINPUT_EVENT_SWITCH_TOGGLE = 900,
}
-
-
+
+ public enum LibInputPointerAxisSource
+ {
+ /**
+ * The event is caused by the rotation of a wheel.
+ **/
+ LIBINPUT_POINTER_AXIS_SOURCE_WHEEL = 1,
+ /**
+ * The event is caused by the movement of one or more fingers on a device.
+ **/
+ LIBINPUT_POINTER_AXIS_SOURCE_FINGER,
+ /**
+ * The event is caused by the motion of some device.
+ **/
+ LIBINPUT_POINTER_AXIS_SOURCE_CONTINUOUS,
+ /**
+ * The event is caused by the tilting of a mouse wheel rather than
+ * its rotation. This method is commonly used on mice without
+ * separate horizontal scroll wheels.
+ * @deprecated This axis source is deprecated as of libinput 1.16.
+ * It was never used by any device before libinput 1.16. All wheel
+ * tilt devices use @ref LIBINPUT_POINTER_AXIS_SOURCE_WHEEL instead.
+ **/
+ LIBINPUT_POINTER_AXIS_SOURCE_WHEEL_TILT,
+ };
+
+ public enum LibInputPointerAxis
+ {
+ LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL = 0,
+ LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL = 1,
+ };
+
[DllImport(LibInput)]
public extern static void libinput_event_destroy(IntPtr ev);
@@ -119,21 +152,29 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
[DllImport(LibInput)]
public extern static IntPtr libinput_event_get_pointer_event(IntPtr ev);
-
-
+
[DllImport(LibInput)]
public extern static ulong libinput_event_pointer_get_time_usec(IntPtr ev);
[DllImport(LibInput)]
public extern static double libinput_event_pointer_get_absolute_x_transformed(IntPtr ev, int width);
-
+
[DllImport(LibInput)]
public extern static double libinput_event_pointer_get_absolute_y_transformed(IntPtr ev, int height);
-
+
[DllImport(LibInput)]
public extern static int libinput_event_pointer_get_button(IntPtr ev);
[DllImport(LibInput)]
public extern static int libinput_event_pointer_get_button_state(IntPtr ev);
+
+ [DllImport(LibInput)]
+ public extern static LibInputPointerAxisSource libinput_event_pointer_get_axis_source(IntPtr ev);
+
+ [DllImport((LibInput))]
+ public extern static double libinput_event_pointer_get_axis_value_discrete(IntPtr ev, LibInputPointerAxis axis);
+
+ [DllImport(LibInput)]
+ public extern static double libinput_event_pointer_get_scroll_value_v120(IntPtr ev, LibInputPointerAxis axis);
}
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
index 730eb0071d..d881a97af2 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
@@ -51,7 +51,7 @@ namespace Avalonia.LinuxFramebuffer
.Bind().ToConstant(new RenderLoop())
.Bind().ToTransient()
.Bind().ToConstant(new KeyboardDevice())
- .Bind().ToSingleton()
+ .Bind().ToSingleton()
.Bind().ToSingleton();
Compositor = new Compositor(
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
index 5c7ec2bbd2..a4d7d8b1eb 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
@@ -51,7 +51,16 @@ namespace Avalonia.LinuxFramebuffer.Output
if(connector == null)
throw new InvalidOperationException("Unable to find connected DRM connector");
- var mode = connector.Modes.OrderByDescending(x => x.IsPreferred)
+ DrmModeInfo? mode = null;
+
+ if (options?.VideoMode != null)
+ {
+ mode = connector.Modes
+ .FirstOrDefault(x => x.Resolution.Width == options.VideoMode.Value.Width &&
+ x.Resolution.Height == options.VideoMode.Value.Height);
+ }
+
+ mode ??= connector.Modes.OrderByDescending(x => x.IsPreferred)
.ThenByDescending(x => x.Resolution.Width * x.Resolution.Height)
//.OrderByDescending(x => x.Resolution.Width * x.Resolution.Height)
.FirstOrDefault();
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs
index dd60d5f09d..c5c7c13c67 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs
@@ -14,13 +14,4 @@ namespace Avalonia.LinuxFramebuffer
public void Dispose() { }
}
}
- internal class PlatformSettings : IPlatformSettings
- {
- public Size DoubleClickSize { get; } = new Size(4, 4);
- public TimeSpan DoubleClickTime { get; } = new TimeSpan(0, 0, 0, 0, 500);
-
- public Size TouchDoubleClickSize => new Size(16,16);
-
- public TimeSpan TouchDoubleClickTime => DoubleClickTime;
- }
}
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs
index 0a9fbcfacb..21c0d97c74 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs
@@ -168,7 +168,11 @@ namespace Avalonia.Markup.Parsers
}
}
- private static State ParseAttachedProperty(scoped ref CharacterReader r, List nodes)
+ private static State ParseAttachedProperty(
+#if NET7SDK
+ scoped
+#endif
+ ref CharacterReader r, List nodes)
{
var (ns, owner) = ParseTypeName(ref r);
@@ -318,7 +322,11 @@ namespace Avalonia.Markup.Parsers
return State.AfterMember;
}
- private static TypeName ParseTypeName(scoped ref CharacterReader r)
+ private static TypeName ParseTypeName(
+#if NET7SDK
+ scoped
+#endif
+ ref CharacterReader r)
{
ReadOnlySpan ns, typeName;
ns = ReadOnlySpan.Empty;
diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
index d11f4aa7d3..71bdc1bd6b 100644
--- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
+++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
@@ -69,10 +69,6 @@ namespace Avalonia.Skia
public int GlyphCount { get; }
- public bool IsFakeBold { get; }
-
- public bool IsFakeItalic { get; }
-
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = default;
diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs
index d700d4848e..857433f95f 100644
--- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs
+++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs
@@ -25,9 +25,9 @@ namespace Avalonia.Skia
public Vector Dpi { get; }
public PixelSize PixelSize { get; }
public int Version { get; private set; }
- public void Save(string fileName) => throw new NotSupportedException();
+ public void Save(string fileName, int? quality = null) => throw new NotSupportedException();
- public void Save(Stream stream) => throw new NotSupportedException();
+ public void Save(Stream stream, int? quality = null) => throw new NotSupportedException();
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
{
diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
index 5fa961cc99..b4dd754822 100644
--- a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
+++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
@@ -14,14 +14,19 @@ namespace Avalonia.Skia.Helpers
///
/// Image to save
/// Target file.
- public static void SaveImage(SKImage image, string fileName)
+ ///
+ /// The optional quality for PNG compression.
+ /// The quality value is interpreted from 0 - 100. If quality is null
+ /// the encoder applies the default quality value.
+ ///
+ public static void SaveImage(SKImage image, string fileName, int? quality = null)
{
if (image == null) throw new ArgumentNullException(nameof(image));
if (fileName == null) throw new ArgumentNullException(nameof(fileName));
using (var stream = File.Create(fileName))
{
- SaveImage(image, stream);
+ SaveImage(image, stream, quality);
}
}
@@ -29,16 +34,30 @@ namespace Avalonia.Skia.Helpers
/// Save Skia image to a stream.
///
/// Image to save
- /// Target stream.
- public static void SaveImage(SKImage image, Stream stream)
+ ///
+ /// The optional quality for PNG compression.
+ /// The quality value is interpreted from 0 - 100. If quality is null
+ /// the encoder applies the default quality value.
+ ///
+ public static void SaveImage(SKImage image, Stream stream, int? quality = null)
{
if (image == null) throw new ArgumentNullException(nameof(image));
if (stream == null) throw new ArgumentNullException(nameof(stream));
- using (var data = image.Encode())
+ if (quality == null)
{
- data.SaveTo(stream);
+ using (var data = image.Encode())
+ {
+ data.SaveTo(stream);
+ }
+ }
+ else
+ {
+ using (var data = image.Encode(SKEncodedImageFormat.Png, (int)quality))
+ {
+ data.SaveTo(stream);
+ }
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
index 6400d67fde..e24d805050 100644
--- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
+++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
@@ -139,13 +139,13 @@ namespace Avalonia.Skia
}
///
- public void Save(string fileName)
+ public void Save(string fileName, int? quality = null)
{
ImageSavingHelper.SaveImage(_image, fileName);
}
///
- public void Save(Stream stream)
+ public void Save(Stream stream, int? quality = null)
{
ImageSavingHelper.SaveImage(_image, stream);
}
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index a9696efbd4..dd3badb2d8 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -12,8 +12,6 @@ using Avalonia.OpenGL.Imaging;
using Avalonia.Platform;
using Avalonia.Media.Imaging;
using SkiaSharp;
-using System.Runtime.InteropServices;
-using System.Drawing;
namespace Avalonia.Skia
{
@@ -79,7 +77,7 @@ namespace Avalonia.Skia
var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize)
{
Size = fontRenderingEmSize,
- Edging = SKFontEdging.Antialias,
+ Edging = SKFontEdging.Alias,
Hinting = SKFontHinting.None,
LinearMetrics = true
};
@@ -244,85 +242,91 @@ namespace Avalonia.Skia
"Current GPU acceleration backend does not support OpenGL integration");
}
- public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- => new SKGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices,
+ IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets)
+ {
+ if (glyphTypeface == null)
+ {
+ throw new ArgumentNullException(nameof(glyphTypeface));
+ }
- public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- => new SKHorizontalGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
+ if (glyphIndices == null)
+ {
+ throw new ArgumentNullException(nameof(glyphIndices));
+ }
- public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- => new SKPositionedGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
+ var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl;
- private abstract class SKGlyphRunBufferBase : IGlyphRunBuffer
- {
- protected readonly SKTextBlobBuilder _builder;
- protected readonly SKFont _font;
+ var font = new SKFont
+ {
+ LinearMetrics = true,
+ Subpixel = true,
+ Edging = SKFontEdging.SubpixelAntialias,
+ Hinting = SKFontHinting.Full,
+ Size = (float)fontRenderingEmSize,
+ Typeface = glyphTypefaceImpl.Typeface,
+ Embolden = (glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0,
+ SkewX = (glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0
+ };
+
+ var builder = new SKTextBlobBuilder();
- public SKGlyphRunBufferBase(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
+ var count = glyphIndices.Count;
+
+ if(glyphOffsets != null && glyphAdvances != null)
{
- _builder = new SKTextBlobBuilder();
+ var runBuffer = builder.AllocatePositionedRun(font, count);
- var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
+ var glyphSpan = runBuffer.GetGlyphSpan();
+ var positionSpan = runBuffer.GetPositionSpan();
- _font = new SKFont
- {
- Subpixel = true,
- Edging = SKFontEdging.SubpixelAntialias,
- Hinting = SKFontHinting.Full,
- LinearMetrics = true,
- Size = fontRenderingEmSize,
- Typeface = glyphTypefaceImpl.Typeface,
- Embolden = glyphTypefaceImpl.IsFakeBold,
- SkewX = glyphTypefaceImpl.IsFakeItalic ? -0.2f : 0
- };
- }
+ var currentX = 0.0;
- public abstract Span GlyphIndices { get; }
+ for (int i = 0; i < glyphOffsets.Count; i++)
+ {
+ var offset = glyphOffsets[i];
- public IGlyphRunImpl Build()
- {
- return new GlyphRunImpl(_builder.Build());
- }
- }
+ glyphSpan[i] = glyphIndices[i];
- private sealed class SKGlyphRunBuffer : SKGlyphRunBufferBase
- {
- private readonly SKRunBuffer _buffer;
+ positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y);
- public SKGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length)
- {
- _buffer = _builder.AllocateRun(_font, length, 0, 0);
+ currentX += glyphAdvances[i];
+ }
}
+ else
+ {
+ if(glyphAdvances != null)
+ {
+ var runBuffer = builder.AllocateHorizontalRun(font, count, 0);
- public override Span GlyphIndices => _buffer.GetGlyphSpan();
- }
+ var glyphSpan = runBuffer.GetGlyphSpan();
+ var positionSpan = runBuffer.GetPositionSpan();
- private sealed class SKHorizontalGlyphRunBuffer : SKGlyphRunBufferBase, IHorizontalGlyphRunBuffer
- {
- private readonly SKHorizontalRunBuffer _buffer;
+ var currentX = 0.0;
- public SKHorizontalGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length)
- {
- _buffer = _builder.AllocateHorizontalRun(_font, length, 0);
- }
+ for (int i = 0; i < glyphOffsets.Count; i++)
+ {
+ glyphSpan[i] = glyphIndices[i];
- public override Span GlyphIndices => _buffer.GetGlyphSpan();
+ positionSpan[i] = (float)currentX;
- public Span GlyphPositions => _buffer.GetPositionSpan();
- }
+ currentX += glyphAdvances[i];
+ }
+ }
+ else
+ {
+ var runBuffer = builder.AllocateRun(font, count, 0, 0);
- private sealed class SKPositionedGlyphRunBuffer : SKGlyphRunBufferBase, IPositionedGlyphRunBuffer
- {
- private readonly SKPositionedRunBuffer _buffer;
+ var glyphSpan = runBuffer.GetGlyphSpan();
- public SKPositionedGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length)
- {
- _buffer = _builder.AllocatePositionedRun(_font, length);
+ for (int i = 0; i < glyphOffsets.Count; i++)
+ {
+ glyphSpan[i] = glyphIndices[i];
+ }
+ }
}
- public override Span GlyphIndices => _buffer.GetGlyphSpan();
-
- public Span GlyphPositions => MemoryMarshal.Cast(_buffer.GetPositionSpan());
+ return new GlyphRunImpl(builder.Build());
}
}
}
diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
index 01b7449b64..d3231c92a5 100644
--- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
+++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
@@ -116,20 +116,20 @@ namespace Avalonia.Skia
public int Version { get; private set; } = 1;
///
- public void Save(string fileName)
+ public void Save(string fileName, int? quality = null)
{
using (var image = SnapshotImage())
{
- ImageSavingHelper.SaveImage(image, fileName);
+ ImageSavingHelper.SaveImage(image, fileName, quality);
}
}
///
- public void Save(Stream stream)
+ public void Save(Stream stream, int? quality = null)
{
using (var image = SnapshotImage())
{
- ImageSavingHelper.SaveImage(image, stream);
+ ImageSavingHelper.SaveImage(image, stream, quality);
}
}
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index b07deb1f4d..eaf588c27d 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
@@ -60,11 +60,11 @@ namespace Avalonia.Skia
var glyphCluster = (int)(sourceInfo.Cluster);
- var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);
+ var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing;
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
- if(glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
+ if(text.Buffer.Span[glyphCluster] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');
diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
index 80bfcc5973..d437f514bb 100644
--- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
+++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
@@ -137,20 +137,20 @@ namespace Avalonia.Skia
}
///
- public void Save(Stream stream)
+ public void Save(Stream stream, int? quality = null)
{
using (var image = GetSnapshot())
{
- ImageSavingHelper.SaveImage(image, stream);
+ ImageSavingHelper.SaveImage(image, stream, quality);
}
}
///
- public void Save(string fileName)
+ public void Save(string fileName, int? quality = null)
{
using (var image = GetSnapshot())
{
- ImageSavingHelper.SaveImage(image, fileName);
+ ImageSavingHelper.SaveImage(image, fileName, quality);
}
}
diff --git a/src/Web/Avalonia.Web/Avalonia.Web.targets b/src/Web/Avalonia.Web/Avalonia.Web.targets
index b6a09b33ef..22363b33d8 100644
--- a/src/Web/Avalonia.Web/Avalonia.Web.targets
+++ b/src/Web/Avalonia.Web/Avalonia.Web.targets
@@ -12,7 +12,7 @@
true
-
+
true
full
true
diff --git a/src/Web/Avalonia.Web/WindowingPlatform.cs b/src/Web/Avalonia.Web/WindowingPlatform.cs
index 7c2a84516b..c399c22c61 100644
--- a/src/Web/Avalonia.Web/WindowingPlatform.cs
+++ b/src/Web/Avalonia.Web/WindowingPlatform.cs
@@ -8,7 +8,7 @@ using Avalonia.Threading;
namespace Avalonia.Web
{
- internal class BrowserWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface
+ internal class BrowserWindowingPlatform : IWindowingPlatform, IPlatformThreadingInterface
{
private bool _signaled;
private static KeyboardDevice? s_keyboard;
@@ -36,7 +36,7 @@ namespace Avalonia.Web
.Bind().ToSingleton()
.Bind().ToSingleton()
.Bind().ToConstant(s_keyboard)
- .Bind().ToConstant(instance)
+ .Bind().ToSingleton()
.Bind().ToConstant(instance)
.Bind().ToConstant(new RenderLoop())
.Bind().ToConstant(ManualTriggerRenderTimer.Instance)
@@ -45,13 +45,6 @@ namespace Avalonia.Web
.Bind().ToSingleton();
}
- public Size DoubleClickSize { get; } = new Size(2, 2);
-
- public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500);
-
- public Size TouchDoubleClickSize => new Size(16, 16);
-
- public TimeSpan TouchDoubleClickTime => DoubleClickTime;
public void RunLoop(CancellationToken cancellationToken)
{
throw new NotSupportedException();
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index 8103f89dad..4d307c9762 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -10,10 +10,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using GlyphRun = Avalonia.Media.GlyphRun;
-using TextAlignment = Avalonia.Media.TextAlignment;
using SharpDX.Mathematics.Interop;
-using System.Runtime.InteropServices;
-using System.Drawing;
namespace Avalonia
{
@@ -160,6 +157,72 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices,
+ IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets)
+ {
+ var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
+
+ var glyphCount = glyphIndices.Count;
+
+ var run = new SharpDX.DirectWrite.GlyphRun
+ {
+ FontFace = glyphTypefaceImpl.FontFace,
+ FontSize = (float)fontRenderingEmSize
+ };
+
+ var indices = new short[glyphCount];
+
+ for (var i = 0; i < glyphCount; i++)
+ {
+ indices[i] = (short)glyphIndices[i];
+ }
+
+ run.Indices = indices;
+
+ run.Advances = new float[glyphCount];
+
+ var scale = (float)(fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight);
+
+ if (glyphAdvances == null)
+ {
+ for (var i = 0; i < glyphCount; i++)
+ {
+ var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[i]) * scale;
+
+ run.Advances[i] = advance;
+ }
+ }
+ else
+ {
+ for (var i = 0; i < glyphCount; i++)
+ {
+ var advance = (float)glyphAdvances[i];
+
+ run.Advances[i] = advance;
+ }
+ }
+
+ if (glyphOffsets == null)
+ {
+ return new GlyphRunImpl(run);
+ }
+
+ run.Offsets = new GlyphOffset[glyphCount];
+
+ for (var i = 0; i < glyphCount; i++)
+ {
+ var (x, y) = glyphOffsets[i];
+
+ run.Offsets[i] = new GlyphOffset
+ {
+ AdvanceOffset = (float)x,
+ AscenderOffset = (float)y
+ };
+ }
+
+ return new GlyphRunImpl(run);
+ }
+
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{
if (glyphRun.GlyphTypeface is not GlyphTypefaceImpl glyphTypeface)
@@ -260,68 +323,6 @@ namespace Avalonia.Direct2D1
return new WicBitmapImpl(format, alphaFormat, data, size, dpi, stride);
}
- private class DWGlyphRunBuffer : IGlyphRunBuffer
- {
- protected readonly SharpDX.DirectWrite.GlyphRun _dwRun;
-
- public DWGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
-
- _dwRun = new SharpDX.DirectWrite.GlyphRun
- {
- FontFace = glyphTypefaceImpl.FontFace,
- FontSize = fontRenderingEmSize,
- Indices = new short[length]
- };
- }
-
- public Span GlyphIndices => MemoryMarshal.Cast(_dwRun.Indices.AsSpan());
-
- public IGlyphRunImpl Build()
- {
- return new GlyphRunImpl(_dwRun);
- }
- }
-
- private class DWHorizontalGlyphRunBuffer : DWGlyphRunBuffer, IHorizontalGlyphRunBuffer
- {
- public DWHorizontalGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- : base(glyphTypeface, fontRenderingEmSize, length)
- {
- _dwRun.Advances = new float[length];
- }
-
- public Span GlyphPositions => _dwRun.Advances.AsSpan();
- }
-
- private class DWPositionedGlyphRunBuffer : DWGlyphRunBuffer, IPositionedGlyphRunBuffer
- {
- public DWPositionedGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- : base(glyphTypeface, fontRenderingEmSize, length)
- {
- _dwRun.Advances = new float[length];
- _dwRun.Offsets = new GlyphOffset[length];
- }
-
- public Span GlyphPositions => MemoryMarshal.Cast(_dwRun.Offsets.AsSpan());
- }
-
- public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- return new DWGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
- }
-
- public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- return new DWHorizontalGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
- }
-
- public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- return new DWPositionedGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
- }
-
public bool SupportsIndividualRoundRects => false;
public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs
index 843efe2cc4..059105c112 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs
@@ -15,7 +15,7 @@ namespace Avalonia.Direct2D1.Media
public abstract OptionalDispose GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target);
- public void Save(string fileName)
+ public void Save(string fileName, int? quality = null)
{
if (Path.GetExtension(fileName) != ".png")
{
@@ -25,11 +25,11 @@ namespace Avalonia.Direct2D1.Media
using (FileStream s = new FileStream(fileName, FileMode.Create))
{
- Save(s);
+ Save(s, quality);
}
}
- public abstract void Save(Stream stream);
+ public abstract void Save(Stream stream, int? quality = null);
public virtual void Dispose()
{
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs
index 2656ab4c58..a321b225a0 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs
@@ -43,7 +43,7 @@ namespace Avalonia.Direct2D1.Media
return new OptionalDispose(_direct2DBitmap, false);
}
- public override void Save(Stream stream)
+ public override void Save(Stream stream, int? quality = null)
{
using (var encoder = new PngBitmapEncoder(Direct2D1Platform.ImagingFactory, stream))
using (var frame = new BitmapFrameEncode(encoder))
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
index 357e472d34..c5f8e837ce 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
@@ -56,7 +56,7 @@ namespace Avalonia.Direct2D1.Media.Imaging
return new OptionalDispose(_renderTarget.Bitmap, false);
}
- public override void Save(Stream stream)
+ public override void Save(Stream stream, int? quality = null)
{
using (var wic = new WicRenderTargetBitmapImpl(PixelSize, Dpi))
{
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
index 1156246b29..051790ef03 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
@@ -186,7 +186,7 @@ namespace Avalonia.Direct2D1.Media
return new OptionalDispose(D2DBitmap.FromWicBitmap(renderTarget, converter), true);
}
- public override void Save(Stream stream)
+ public override void Save(Stream stream, int? quality = null)
{
using (var encoder = new PngBitmapEncoder(Direct2D1Platform.ImagingFactory, stream))
using (var frame = new BitmapFrameEncode(encoder))
diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
index 064320f809..7f2cbc6182 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
@@ -64,7 +64,7 @@ namespace Avalonia.Direct2D1.Media
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
- if (glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
+ if (text.Buffer.Span[glyphCluster] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');
diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
index 0d29bb91ea..308e22b4e2 100644
--- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
+++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index f1ed53ea99..9b86154043 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -1083,6 +1083,20 @@ namespace Avalonia.Win32.Interop
public const int SizeOf_BITMAPINFOHEADER = 40;
+ [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto, SetLastError = true)]
+ unsafe internal static extern int GetMouseMovePointsEx(
+ uint cbSize, MOUSEMOVEPOINT* pointsIn,
+ MOUSEMOVEPOINT* pointsBufferOut, int nBufPoints, uint resolution);
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] // For GetMouseMovePointsEx
+ public struct MOUSEMOVEPOINT
+ {
+ public int x; //Specifies the x-coordinate of the mouse
+ public int y; //Specifies the x-coordinate of the mouse
+ public int time; //Specifies the time stamp of the mouse coordinate
+ public IntPtr dwExtraInfo; //Specifies extra information associated with this coordinate.
+ }
+
[DllImport("user32.dll", SetLastError = true)]
public static extern bool IsMouseInPointerEnabled();
@@ -2104,6 +2118,12 @@ namespace Avalonia.Win32.Interop
public int Y;
}
+ public struct SIZE_F
+ {
+ public float X;
+ public float Y;
+ }
+
public struct RECT
{
public int left;
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index 3cdc3586dc..18977ec4c3 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -130,17 +130,6 @@ namespace Avalonia.Win32
internal static Compositor Compositor { get; private set; }
- public Size DoubleClickSize => new Size(
- UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK),
- UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYDOUBLECLK));
-
- public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(UnmanagedMethods.GetDoubleClickTime());
-
- ///
- public Size TouchDoubleClickSize => new Size(16,16);
-
- ///
- public TimeSpan TouchDoubleClickTime => DoubleClickTime;
public static void Initialize()
{
Initialize(new Win32PlatformOptions());
@@ -398,5 +387,25 @@ namespace Avalonia.Win32
SetProcessDPIAware();
}
+
+ Size IPlatformSettings.GetTapSize(PointerType type)
+ {
+ return type switch
+ {
+ PointerType.Touch => new(10, 10),
+ _ => new(GetSystemMetrics(SystemMetric.SM_CXDRAG), GetSystemMetrics(SystemMetric.SM_CYDRAG)),
+ };
+ }
+
+ Size IPlatformSettings.GetDoubleTapSize(PointerType type)
+ {
+ return type switch
+ {
+ PointerType.Touch => new(16, 16),
+ _ => new(GetSystemMetrics(SystemMetric.SM_CXDOUBLECLK), GetSystemMetrics(SystemMetric.SM_CYDOUBLECLK)),
+ };
+ }
+
+ TimeSpan IPlatformSettings.GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(GetDoubleClickTime());
}
}
diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
index 8a41c00add..6da17b8ea5 100644
--- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
+++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
@@ -174,8 +174,9 @@ namespace Avalonia.Win32.WinRT.Composition
using var sc = _syncContext.EnsureLocked();
using var desktopTarget = _compositorDesktopInterop.CreateDesktopWindowTarget(hWnd, 0);
using var target = desktopTarget.QueryInterface();
+ using var device2 = _device.QueryInterface();
- using var drawingSurface = _device.CreateDrawingSurface(new UnmanagedMethods.SIZE(), DirectXPixelFormat.B8G8R8A8UIntNormalized,
+ using var drawingSurface = device2.CreateDrawingSurface2(new UnmanagedMethods.SIZE(), DirectXPixelFormat.B8G8R8A8UIntNormalized,
DirectXAlphaMode.Premultiplied);
using var surface = drawingSurface.QueryInterface();
using var surfaceInterop = drawingSurface.QueryInterface();
diff --git a/src/Windows/Avalonia.Win32/WinRT/winrt.idl b/src/Windows/Avalonia.Win32/WinRT/winrt.idl
index 851df9dae6..ffb98fafc8 100644
--- a/src/Windows/Avalonia.Win32/WinRT/winrt.idl
+++ b/src/Windows/Avalonia.Win32/WinRT/winrt.idl
@@ -8,6 +8,7 @@
@clr-map Matrix4x4 System.Numerics.Matrix4x4
@clr-map RECT Avalonia.Win32.Interop.UnmanagedMethods.RECT
@clr-map SIZE Avalonia.Win32.Interop.UnmanagedMethods.SIZE
+@clr-map SIZE_F Avalonia.Win32.Interop.UnmanagedMethods.SIZE_F
@clr-map POINT Avalonia.Win32.Interop.UnmanagedMethods.POINT
@clr-map HWND IntPtr
@clr-map BOOL int
@@ -442,12 +443,18 @@ interface IDesktopWindowContentBridgeInterop : IUnknown
[uuid(FB22C6E1-80A2-4667-9936-DBEAF6EEFE95)]
interface ICompositionGraphicsDevice : IInspectable
{
- HRESULT CreateDrawingSurface([in] SIZE sizePixels, [in] DirectXPixelFormat pixelFormat,
+ HRESULT CreateDrawingSurface([in] SIZE_F sizePixels, [in] DirectXPixelFormat pixelFormat,
[in] DirectXAlphaMode alphaMode, [out] [retval] ICompositionDrawingSurface** result);
HRESULT AddRenderingDeviceReplaced(void* handler, void* token);
HRESULT RemoveRenderingDeviceReplaced([in] int token);
}
+[uuid(0FB8BDF6-C0F0-4BCC-9FB8-084982490D7D)]
+interface ICompositionGraphicsDevice2 : IInspectable
+{
+ HRESULT CreateDrawingSurface2([in] SIZE sizePixels, [in] DirectXPixelFormat pixelFormat, [in] DirectXAlphaMode alphaMode, [out] [retval] ICompositionDrawingSurface** result);
+}
+
[uuid(1527540D-42C7-47A6-A408-668F79A90DFB)]
interface ICompositionSurface : IInspectable
{
@@ -465,7 +472,7 @@ interface ICompositionDrawingSurface : IInspectable
{
[propget] HRESULT GetAlphaMode([out] [retval] DirectXAlphaMode* value);
[propget] HRESULT GetPixelFormat([out] [retval] DirectXPixelFormat* value);
- [propget] HRESULT GetSize([out] [retval] POINT* value);
+ [propget] HRESULT GetSize([out] [retval] SIZE_F* value);
}
enum CompositionBitmapInterpolationMode
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
index f8785371d9..c4136a239e 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
@@ -272,13 +272,34 @@ namespace Avalonia.Win32
TrackMouseEvent(ref tm);
}
+ var point = DipFromLParam(lParam);
+
+ // Prepare points for the IntermediatePoints call.
+ var p = new POINT()
+ {
+ X = (int)(point.X * RenderScaling),
+ Y = (int)(point.Y * RenderScaling)
+ };
+ ClientToScreen(_hwnd, ref p);
+ var currPoint = new MOUSEMOVEPOINT()
+ {
+ x = p.X & 0xFFFF,
+ y = p.Y & 0xFFFF,
+ time = (int)timestamp
+ };
+ var prevPoint = _lastWmMousePoint;
+ _lastWmMousePoint = currPoint;
+
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
RawPointerEventType.Move,
- DipFromLParam(lParam),
- GetMouseModifiers(wParam));
+ point,
+ GetMouseModifiers(wParam))
+ {
+ IntermediatePoints = new Lazy>(() => CreateLazyIntermediatePoints(currPoint, prevPoint))
+ };
break;
}
@@ -782,6 +803,65 @@ namespace Avalonia.Win32
return null;
}
+ private unsafe IReadOnlyList CreateLazyIntermediatePoints(MOUSEMOVEPOINT movePoint, MOUSEMOVEPOINT prevMovePoint)
+ {
+ // To understand some of this code, please check MS docs:
+ // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmousemovepointsex#remarks
+
+ fixed (MOUSEMOVEPOINT* movePoints = s_mouseHistoryInfos)
+ {
+ var movePointCopy = movePoint;
+ movePointCopy.time = 0; // empty "time" as otherwise WinAPI will always fail
+ int pointsCount = GetMouseMovePointsEx(
+ (uint)(Marshal.SizeOf(movePointCopy)),
+ &movePointCopy, movePoints, s_mouseHistoryInfos.Length,
+ 1);
+
+ // GetMouseMovePointsEx can return -1 if point wasn't found or there is so beeg delay that original points were erased from the buffer.
+ if (pointsCount <= 1)
+ {
+ return Array.Empty();
+ }
+
+ s_intermediatePointsPooledList.Clear();
+ s_intermediatePointsPooledList.Capacity = pointsCount;
+ for (int i = pointsCount - 1; i >= 1; i--)
+ {
+ var historyInfo = s_mouseHistoryInfos[i];
+ // Skip points newer than current point.
+ if (historyInfo.time > movePoint.time ||
+ (historyInfo.time == movePoint.time &&
+ historyInfo.x == movePoint.x &&
+ historyInfo.y == movePoint.y))
+ {
+ continue;
+ }
+ // Skip poins older from previous WM_MOUSEMOVE point.
+ if (historyInfo.time < prevMovePoint.time ||
+ (historyInfo.time == prevMovePoint.time &&
+ historyInfo.x == prevMovePoint.x &&
+ historyInfo.y == prevMovePoint.y))
+ {
+ continue;
+ }
+
+ // To support multiple screens.
+ if (historyInfo.x > 32767)
+ historyInfo.x -= 65536;
+
+ if (historyInfo.y > 32767)
+ historyInfo.y -= 65536;
+
+ var point = PointToClient(new PixelPoint(historyInfo.x, historyInfo.y));
+ s_intermediatePointsPooledList.Add(new RawPointerPoint
+ {
+ Position = point
+ });
+ }
+ return s_intermediatePointsPooledList;
+ }
+ }
+
private RawPointerEventArgs CreatePointerArgs(IInputDevice device, ulong timestamp, RawPointerEventType eventType, RawPointerPoint point, RawInputModifiers modifiers, uint rawPointerId)
{
return device is TouchDevice
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 5374614379..9ed1a50ff2 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -65,6 +65,7 @@ namespace Avalonia.Win32
private bool _isUsingComposition;
private IBlurHost _blurHost;
private PlatformResizeReason _resizeReason;
+ private MOUSEMOVEPOINT _lastWmMousePoint;
#if USE_MANAGED_DRAG
private readonly ManagedWindowResizeDragHelper _managedDrag;
@@ -107,6 +108,7 @@ namespace Avalonia.Win32
private static readonly POINTER_TOUCH_INFO[] s_historyTouchInfos = new POINTER_TOUCH_INFO[MaxPointerHistorySize];
private static readonly POINTER_PEN_INFO[] s_historyPenInfos = new POINTER_PEN_INFO[MaxPointerHistorySize];
private static readonly POINTER_INFO[] s_historyInfos = new POINTER_INFO[MaxPointerHistorySize];
+ private static readonly MOUSEMOVEPOINT[] s_mouseHistoryInfos = new MOUSEMOVEPOINT[64];
public WindowImpl()
{
diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs
index 67d037f9e5..c2cf8c6f6c 100644
--- a/src/iOS/Avalonia.iOS/Platform.cs
+++ b/src/iOS/Avalonia.iOS/Platform.cs
@@ -28,32 +28,19 @@ namespace Avalonia.iOS
public static EaglFeature GlFeature;
public static DisplayLinkTimer Timer;
internal static Compositor Compositor { get; private set; }
-
- class PlatformSettings : IPlatformSettings
- {
- ///
- public Size TouchDoubleClickSize => new Size(10, 10);
-
- ///
- public TimeSpan TouchDoubleClickTime => TimeSpan.FromMilliseconds(500);
-
- public Size DoubleClickSize => new Size(4, 4);
-
- public TimeSpan DoubleClickTime => TouchDoubleClickTime;
- }
public static void Register()
{
GlFeature ??= new EaglFeature();
Timer ??= new DisplayLinkTimer();
var keyboard = new KeyboardDevice();
-
+
AvaloniaLocator.CurrentMutable
.Bind().ToConstant(GlFeature)
.Bind().ToConstant(new CursorFactoryStub())
.Bind().ToConstant(new WindowingPlatformStub())
.Bind().ToConstant(new ClipboardImpl())
- .Bind().ToConstant(new PlatformSettings())
+ .Bind().ToSingleton()
.Bind().ToConstant(new PlatformIconLoaderStub())
.Bind().ToSingleton()
.Bind().ToSingleton()
diff --git a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs
index 7b7d547346..c0c0182622 100644
--- a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs
@@ -209,7 +209,9 @@ namespace Avalonia.Input.UnitTests
var unitTestApp = UnitTestApplication.Start(
new TestServices(inputManager: new InputManager()));
var iSettingsMock = new Mock();
- iSettingsMock.Setup(x => x.TouchDoubleClickTime).Returns(doubleClickTime);
+ iSettingsMock.Setup(x => x.GetDoubleTapTime(It.IsAny())).Returns(doubleClickTime);
+ iSettingsMock.Setup(x => x.GetDoubleTapSize(It.IsAny())).Returns(new Size(16, 16));
+ iSettingsMock.Setup(x => x.GetTapSize(It.IsAny())).Returns(new Size(16, 16));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind().ToConstant(iSettingsMock.Object);
return unitTestApp;
diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
index 5f8eb45f71..10db08f302 100644
--- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
@@ -72,7 +72,7 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException();
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets)
{
throw new NotImplementedException();
}
@@ -126,21 +126,6 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException();
}
- public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- throw new NotImplementedException();
- }
-
- public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- throw new NotImplementedException();
- }
-
- public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- throw new NotImplementedException();
- }
-
class MockStreamGeometry : IStreamGeometryImpl
{
private MockStreamGeometryContext _impl = new MockStreamGeometryContext();
diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
index 34f0dfef11..4170de71e6 100644
--- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
+++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
@@ -5,6 +5,7 @@ using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Avalonia.Media.Imaging;
+using Microsoft.Diagnostics.Runtime;
namespace Avalonia.Benchmarks
{
@@ -117,19 +118,9 @@ namespace Avalonia.Benchmarks
return new MockStreamGeometryImpl();
}
- public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets)
{
- throw new NotImplementedException();
- }
-
- public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- throw new NotImplementedException();
- }
-
- public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
- {
- throw new NotImplementedException();
+ return new MockGlyphRun();
}
public bool SupportsIndividualRoundRects => true;
diff --git a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
deleted file mode 100644
index 05007e4f2e..0000000000
--- a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using Avalonia.Controls.Documents;
-using Avalonia.Controls.Presenters;
-using Avalonia.Controls.Templates;
-using Avalonia.Media;
-using Avalonia.UnitTests;
-using Xunit;
-
-namespace Avalonia.Controls.UnitTests
-{
- public class RichTextBlockTests
- {
- [Fact]
- public void Changing_InlinesCollection_Should_Invalidate_Measure()
- {
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
- {
- var target = new RichTextBlock();
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
-
- target.Inlines.Add(new Run("Hello"));
-
- Assert.False(target.IsMeasureValid);
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
- }
- }
-
- [Fact]
- public void Changing_Inlines_Properties_Should_Invalidate_Measure()
- {
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
- {
- var target = new RichTextBlock();
-
- var inline = new Run("Hello");
-
- target.Inlines.Add(inline);
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
-
- inline.Foreground = Brushes.Green;
-
- Assert.False(target.IsMeasureValid);
- }
- }
-
- [Fact]
- public void Changing_Inlines_Should_Invalidate_Measure()
- {
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
- {
- var target = new RichTextBlock();
-
- var inlines = new InlineCollection { new Run("Hello") };
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
-
- target.Inlines = inlines;
-
- Assert.False(target.IsMeasureValid);
- }
- }
-
- [Fact]
- public void Changing_Inlines_Should_Reset_Inlines_Parent()
- {
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
- {
- var target = new RichTextBlock();
-
- var run = new Run("Hello");
-
- target.Inlines.Add(run);
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
-
- target.Inlines = null;
-
- Assert.Null(run.Parent);
-
- target.Inlines = new InlineCollection { run };
-
- Assert.Equal(target, run.Parent);
- }
- }
-
- [Fact]
- public void InlineUIContainer_Child_Schould_Be_Arranged()
- {
- using (UnitTestApplication.Start(TestServices.StyledWindow))
- {
- var target = new RichTextBlock();
-
- var button = new Button { Content = "12345678" };
-
- button.Template = new FuncControlTemplate((parent, scope) =>
- new TextBlock
- {
- Name = "PART_ContentPresenter",
- [!TextBlock.TextProperty] = parent[!ContentControl.ContentProperty],
- }.RegisterInNameScope(scope)
- );
-
- target.Inlines!.Add("123456");
- target.Inlines.Add(new InlineUIContainer(button));
- target.Inlines.Add("123456");
-
- target.Measure(Size.Infinity);
-
- Assert.True(button.IsMeasureValid);
- Assert.Equal(80, button.DesiredSize.Width);
-
- target.Arrange(new Rect(new Size(200, 50)));
-
- Assert.True(button.IsArrangeValid);
-
- Assert.Equal(60, button.Bounds.Left);
- }
- }
- }
-}
diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
index 37dde9fbac..de5e5a8ea3 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
@@ -1,7 +1,9 @@
using System;
using Avalonia.Controls.Documents;
+using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Media;
+using Avalonia.Metadata;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Moq;
@@ -60,5 +62,146 @@ namespace Avalonia.Controls.UnitTests
renderer.Verify(x => x.AddDirty(target), Times.Once);
}
+
+ [Fact]
+ public void Changing_InlinesCollection_Should_Invalidate_Measure()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ {
+ var target = new TextBlock();
+
+ target.Measure(Size.Infinity);
+
+ Assert.True(target.IsMeasureValid);
+
+ target.Inlines.Add(new Run("Hello"));
+
+ Assert.False(target.IsMeasureValid);
+
+ target.Measure(Size.Infinity);
+
+ Assert.True(target.IsMeasureValid);
+ }
+ }
+
+ [Fact]
+ public void Changing_Inlines_Properties_Should_Invalidate_Measure()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ {
+ var target = new TextBlock();
+
+ var inline = new Run("Hello");
+
+ target.Inlines.Add(inline);
+
+ target.Measure(Size.Infinity);
+
+ Assert.True(target.IsMeasureValid);
+
+ inline.Foreground = Brushes.Green;
+
+ Assert.False(target.IsMeasureValid);
+ }
+ }
+
+ [Fact]
+ public void Changing_Inlines_Should_Invalidate_Measure()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ {
+ var target = new TextBlock();
+
+ var inlines = new InlineCollection { new Run("Hello") };
+
+ target.Measure(Size.Infinity);
+
+ Assert.True(target.IsMeasureValid);
+
+ target.Inlines = inlines;
+
+ Assert.False(target.IsMeasureValid);
+ }
+ }
+
+ [Fact]
+ public void Changing_Inlines_Should_Reset_Inlines_Parent()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ {
+ var target = new TextBlock();
+
+ var run = new Run("Hello");
+
+ target.Inlines.Add(run);
+
+ target.Measure(Size.Infinity);
+
+ Assert.True(target.IsMeasureValid);
+
+ target.Inlines = null;
+
+ Assert.Null(run.Parent);
+
+ target.Inlines = new InlineCollection { run };
+
+ Assert.Equal(target, run.Parent);
+ }
+ }
+
+ [Fact]
+ public void InlineUIContainer_Child_Schould_Be_Arranged()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var target = new TextBlock();
+
+ var button = new Button { Content = "12345678" };
+
+ button.Template = new FuncControlTemplate((parent, scope) =>
+ new TextBlock
+ {
+ Name = "PART_ContentPresenter",
+ [!TextBlock.TextProperty] = parent[!ContentControl.ContentProperty],
+ }.RegisterInNameScope(scope)
+ );
+
+ target.Inlines!.Add("123456");
+ target.Inlines.Add(new InlineUIContainer(button));
+ target.Inlines.Add("123456");
+
+ target.Measure(Size.Infinity);
+
+ Assert.True(button.IsMeasureValid);
+ Assert.Equal(80, button.DesiredSize.Width);
+
+ target.Arrange(new Rect(new Size(200, 50)));
+
+ Assert.True(button.IsArrangeValid);
+
+ Assert.Equal(60, button.Bounds.Left);
+ }
+ }
+
+ [Fact]
+ public void Setting_Text_Should_Reset_Inlines()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var target = new TextBlock();
+
+ target.Inlines.Add(new Run("Hello World"));
+
+ Assert.Equal("Hello World", target.Text);
+
+ Assert.Equal(1, target.Inlines.Count);
+
+ target.Text = "1234";
+
+ Assert.Equal("1234", target.Text);
+
+ Assert.Equal(0, target.Inlines.Count);
+ }
+ }
}
}
diff --git a/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs b/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs
new file mode 100644
index 0000000000..205ad6b8cf
--- /dev/null
+++ b/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Threading;
+using OpenQA.Selenium.Appium;
+using OpenQA.Selenium.Interactions;
+using Xunit;
+
+namespace Avalonia.IntegrationTests.Appium
+{
+ [Collection("Default")]
+ public class GestureTests
+ {
+ private readonly AppiumDriver _session;
+
+ public GestureTests(TestAppFixture fixture)
+ {
+ _session = fixture.Session;
+
+ var tabs = _session.FindElementByAccessibilityId("MainTabs");
+ var tab = tabs.FindElementByName("Gestures");
+ tab.Click();
+ var clear = _session.FindElementByAccessibilityId("ResetGestures");
+ clear.Click();
+ }
+
+ [Fact]
+ public void Tapped_Is_Raised()
+ {
+ var border = _session.FindElementByAccessibilityId("GestureBorder");
+ var lastGesture = _session.FindElementByAccessibilityId("LastGesture");
+
+ new Actions(_session).Click(border).Perform();
+
+ Assert.Equal("Tapped", lastGesture.Text);
+ }
+
+ [Fact]
+ public void Tapped_Is_Raised_Slow()
+ {
+ var border = _session.FindElementByAccessibilityId("GestureBorder");
+ var lastGesture = _session.FindElementByAccessibilityId("LastGesture");
+
+ new Actions(_session).ClickAndHold(border).Perform();
+
+ Thread.Sleep(2000);
+
+ new Actions(_session).Release(border).Perform();
+
+ Assert.Equal("Tapped", lastGesture.Text);
+ }
+
+ [Fact]
+ public void Tapped_Is_Not_Raised_For_Drag()
+ {
+ var border = _session.FindElementByAccessibilityId("GestureBorder");
+ var lastGesture = _session.FindElementByAccessibilityId("LastGesture");
+
+ new Actions(_session)
+ .ClickAndHold(border)
+ .MoveByOffset(50, 50)
+ .Release()
+ .Perform();
+
+ Assert.Equal(string.Empty, lastGesture.Text);
+ }
+
+ [Fact]
+ public void DoubleTapped_Is_Raised()
+ {
+ var border = _session.FindElementByAccessibilityId("GestureBorder");
+ var lastGesture = _session.FindElementByAccessibilityId("LastGesture");
+
+ new Actions(_session).DoubleClick(border).Perform();
+
+ Assert.Equal("DoubleTapped", lastGesture.Text);
+ }
+
+ [Fact]
+ public void DoubleTapped_Is_Raised_2()
+ {
+ var border = _session.FindElementByAccessibilityId("GestureBorder");
+ var lastGesture = _session.FindElementByAccessibilityId("LastGesture");
+
+ new Actions(_session).ClickAndHold(border).Release().Perform();
+
+ Thread.Sleep(100);
+
+ // DoubleTapped is raised on second pointer press, not release.
+ new Actions(_session).ClickAndHold(border).Perform();
+
+ try
+ {
+ Assert.Equal("DoubleTapped", lastGesture.Text);
+ }
+ finally
+ {
+ new Actions(_session).Release(border).Perform();
+ }
+ }
+
+ [Fact]
+ public void DoubleTapped_Is_Raised_Not_Raised_If_Too_Slow()
+ {
+ var border = _session.FindElementByAccessibilityId("GestureBorder");
+ var lastGesture = _session.FindElementByAccessibilityId("LastGesture");
+
+ new Actions(_session).ClickAndHold(border).Release().Perform();
+
+ Thread.Sleep(2000);
+
+ new Actions(_session).ClickAndHold(border).Release().Perform();
+
+ Assert.Equal("Tapped", lastGesture.Text);
+ }
+
+ [Fact]
+ public void DoubleTapped_Is_Raised_After_Control_Changes()
+ {
+ // #8733
+ var border = _session.FindElementByAccessibilityId("GestureBorder");
+ var lastGesture = _session.FindElementByAccessibilityId("LastGesture");
+
+ new Actions(_session)
+ .MoveToElement(border)
+ .DoubleClick()
+ .DoubleClick()
+ .Perform();
+
+ Assert.Equal("DoubleTapped2", lastGesture.Text);
+ }
+
+ [Fact]
+ public void RightTapped_Is_Raised()
+ {
+ var border = _session.FindElementByAccessibilityId("GestureBorder");
+ var lastGesture = _session.FindElementByAccessibilityId("LastGesture");
+
+ new Actions(_session).ContextClick(border).Perform();
+
+ Assert.Equal("RightTapped", lastGesture.Text);
+ }
+
+ [PlatformFact(TestPlatforms.MacOS)]
+ public void RightTapped_Is_Raised_2()
+ {
+ var border = _session.FindElementByAccessibilityId("GestureBorder");
+ var lastGesture = _session.FindElementByAccessibilityId("LastGesture");
+ var device = new PointerInputDevice(PointerKind.Mouse);
+ var b = new ActionBuilder();
+
+ b.AddAction(device.CreatePointerMove(border, 50, 50, TimeSpan.FromMilliseconds(100)));
+ b.AddAction(device.CreatePointerDown(MouseButton.Right));
+ b.AddAction(device.CreatePointerMove(border, 2, 2, TimeSpan.FromMilliseconds(100)));
+ b.AddAction(device.CreatePointerUp(MouseButton.Right));
+ _session.PerformActions(b.ToActionSequenceList());
+
+ Assert.Equal("RightTapped", lastGesture.Text);
+ }
+
+ [PlatformFact(TestPlatforms.MacOS)]
+ public void RightTapped_Is_Not_Raised_For_Drag()
+ {
+ var border = _session.FindElementByAccessibilityId("GestureBorder");
+ var lastGesture = _session.FindElementByAccessibilityId("LastGesture");
+ var device = new PointerInputDevice(PointerKind.Mouse);
+ var b = new ActionBuilder();
+
+ b.AddAction(device.CreatePointerMove(border, 50, 50, TimeSpan.FromMilliseconds(100)));
+ b.AddAction(device.CreatePointerDown(MouseButton.Right));
+ b.AddAction(device.CreatePointerMove(CoordinateOrigin.Pointer, 50, 50, TimeSpan.FromMilliseconds(100)));
+ b.AddAction(device.CreatePointerUp(MouseButton.Right));
+
+ Assert.Equal(string.Empty, lastGesture.Text);
+ }
+ }
+}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index a315158e1b..316926b00c 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -425,7 +425,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(flowDirection, textAlignment, true, true,
- defaultProperties, TextWrapping.NoWrap, 0, 0);
+ defaultProperties, TextWrapping.NoWrap, 0, 0, 0);
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index 33c18c5064..87de9ed11f 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -878,7 +878,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, 0, 200,
- new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0));
+ new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left,
+ true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
var textBounds = textLine.GetTextBounds(0, 3);
@@ -924,7 +925,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, 0, 200,
- new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0));
+ new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left,
+ true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
var textBounds = textLine.GetTextBounds(0, 4);
diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs
new file mode 100644
index 0000000000..24948aff01
--- /dev/null
+++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs
@@ -0,0 +1,12 @@
+using Avalonia.Platform;
+
+namespace Avalonia.UnitTests
+{
+ public class MockGlyphRun : IGlyphRunImpl
+ {
+ public void Dispose()
+ {
+
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
index 586436ef7f..0f951ed867 100644
--- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
+++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
@@ -142,7 +142,7 @@ namespace Avalonia.UnitTests
throw new NotImplementedException();
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets)
{
return Mock.Of();
}