Browse Source

Merge remote-tracking branch 'origin/master' into feature/onplatform-xaml-compiler-support

pull/7812/head
Max Katz 4 years ago
parent
commit
5484bf4ac2
  1. 5
      .ncrunch/Avalonia.Web.Blazor.v3.ncrunchproject
  2. 5
      .ncrunch/Avalonia.Web.v3.ncrunchproject
  3. 6
      .ncrunch/Avalonia.Win32.net6.0.v3.ncrunchproject
  4. 6
      .ncrunch/Avalonia.Win32.netstandard2.0.v3.ncrunchproject
  5. 5
      .ncrunch/ControlCatalog.Blazor.Web.v3.ncrunchproject
  6. 5
      .ncrunch/MobileSandbox.Android.v3.ncrunchproject
  7. 5
      .ncrunch/MobileSandbox.Desktop.v3.ncrunchproject
  8. 5
      .ncrunch/MobileSandbox.iOS.v3.ncrunchproject
  9. 5
      .ncrunch/MobileSandbox.net6.0.v3.ncrunchproject
  10. 5
      .ncrunch/MobileSandbox.netstandard2.0.v3.ncrunchproject
  11. 5
      .ncrunch/_build.v3.ncrunchproject
  12. 60
      Avalonia.Desktop.slnf
  13. 1
      Avalonia.sln
  14. 5
      Directory.Build.targets
  15. 38
      Documentation/build.md
  16. 12
      azure-pipelines.yml
  17. 2
      global.json
  18. 4
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  19. 4
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  20. 11
      native/Avalonia.Native/src/OSX/app.mm
  21. 2
      native/Avalonia.Native/src/OSX/common.h
  22. 41
      native/Avalonia.Native/src/OSX/main.mm
  23. 5
      native/Avalonia.Native/src/OSX/rendertarget.mm
  24. 19
      nukebuild/Build.cs
  25. 6
      nukebuild/_build.csproj
  26. 2
      readme.md
  27. 4
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  28. 17
      samples/IntegrationTestApp/MainWindow.axaml
  29. 34
      samples/IntegrationTestApp/MainWindow.axaml.cs
  30. 1
      samples/MobileSandbox/MainView.xaml
  31. 46
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  32. 14
      src/Android/Avalonia.Android/AndroidPlatform.cs
  33. 114
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  34. 2
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
  35. 6
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  36. 16
      src/Avalonia.Base/Controls/NameScopeEventArgs.cs
  37. 2
      src/Avalonia.Base/Input/DragEventArgs.cs
  38. 25
      src/Avalonia.Base/Input/Gestures.cs
  39. 5
      src/Avalonia.Base/Input/GotFocusEventArgs.cs
  40. 5
      src/Avalonia.Base/Input/KeyEventArgs.cs
  41. 6
      src/Avalonia.Base/Input/MouseDevice.cs
  42. 5
      src/Avalonia.Base/Input/PenDevice.cs
  43. 2
      src/Avalonia.Base/Input/PointerDeltaEventArgs.cs
  44. 12
      src/Avalonia.Base/Input/PointerEventArgs.cs
  45. 2
      src/Avalonia.Base/Input/PointerWheelEventArgs.cs
  46. 6
      src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
  47. 2
      src/Avalonia.Base/Input/TappedEventArgs.cs
  48. 1
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  49. 4
      src/Avalonia.Base/Input/TextInputEventArgs.cs
  50. 7
      src/Avalonia.Base/Input/TouchDevice.cs
  51. 5
      src/Avalonia.Base/Input/VectorEventArgs.cs
  52. 30
      src/Avalonia.Base/Interactivity/RoutedEventArgs.cs
  53. 2
      src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs
  54. 6
      src/Avalonia.Base/Layout/UniformGridLayoutState.cs
  55. 3
      src/Avalonia.Base/Media/FormattedText.cs
  56. 77
      src/Avalonia.Base/Media/GlyphRun.cs
  57. 18
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  58. 14
      src/Avalonia.Base/Media/Imaging/IBitmap.cs
  59. 12
      src/Avalonia.Base/Media/PathMarkupParser.cs
  60. 2
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  61. 19
      src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
  62. 7
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  63. 21
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  64. 9
      src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
  65. 9
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  66. 30
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  67. 14
      src/Avalonia.Base/Platform/IBitmapImpl.cs
  68. 37
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  69. 22
      src/Avalonia.Base/Platform/IPlatformSettings.cs
  70. 2
      src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs
  71. 6
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  72. 83
      src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs
  73. 8
      src/Avalonia.Controls/Automation/AutomationProperties.cs
  74. 14
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  75. 16
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  76. 41
      src/Avalonia.Controls/Control.cs
  77. 91
      src/Avalonia.Controls/Documents/InlineCollection.cs
  78. 6
      src/Avalonia.Controls/Documents/Span.cs
  79. 49
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  80. 19
      src/Avalonia.Controls/ProgressBar.cs
  81. 374
      src/Avalonia.Controls/SelectableTextBlock.cs
  82. 83
      src/Avalonia.Controls/SizeChangedEventArgs.cs
  83. 312
      src/Avalonia.Controls/TextBlock.cs
  84. 14
      src/Avalonia.Controls/TextBox.cs
  85. 11
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  86. 2
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  87. 46
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  88. 10
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  89. 2
      src/Avalonia.MicroCom/Avalonia.MicroCom.csproj
  90. 2
      src/Avalonia.Native/Avalonia.Native.csproj
  91. 16
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  92. 8
      src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
  93. 6
      src/Avalonia.Native/DoubleClickHelper.cs
  94. 2
      src/Avalonia.Native/avn.idl
  95. 2
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  96. 14
      src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml
  97. 18
      src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml
  98. 7
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  99. 14
      src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml
  100. 18
      src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml

5
.ncrunch/Avalonia.Web.Blazor.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.Web.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

6
.ncrunch/Avalonia.Win32.net6.0.v3.ncrunchproject

@ -1,7 +1,3 @@
<ProjectConfiguration>
<Settings>
<AdditionalFilesToIncludeForProject>
<Value>..\..\tools\MicroComGenerator\bin\Debug\net6.0\**.*</Value>
</AdditionalFilesToIncludeForProject>
</Settings>
<Settings />
</ProjectConfiguration>

6
.ncrunch/Avalonia.Win32.netstandard2.0.v3.ncrunchproject

@ -1,7 +1,3 @@
<ProjectConfiguration>
<Settings>
<AdditionalFilesToIncludeForProject>
<Value>..\..\tools\MicroComGenerator\bin\Debug\net6.0\**.*</Value>
</AdditionalFilesToIncludeForProject>
</Settings>
<Settings />
</ProjectConfiguration>

5
.ncrunch/ControlCatalog.Blazor.Web.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/MobileSandbox.Android.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/MobileSandbox.Desktop.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/MobileSandbox.iOS.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/MobileSandbox.net6.0.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/MobileSandbox.netstandard2.0.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/_build.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

60
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"
]
}
}

1
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

5
Directory.Build.targets

@ -0,0 +1,5 @@
<Project>
<PropertyGroup Condition="$(NETCoreSdkVersion.StartsWith('7.0'))">
<DefineConstants>$(DefineConstants);NET7SDK</DefineConstants>
</PropertyGroup>
</Project>

38
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

12
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'

2
global.json

@ -1,6 +1,6 @@
{
"sdk": {
"version": "7.0.100-rc.2.22477.23",
"version": "7.0.100",
"rollForward": "latestFeature"
},
"msbuild-sdks": {

4
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

4
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<WindowImpl *>(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

11
native/Avalonia.Native/src/OSX/app.mm

@ -95,11 +95,14 @@ ComPtr<IAvnApplicationEvents> _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()

2
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);

41
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;
};

5
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(), ^{

19
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)

6
nukebuild/_build.csproj

@ -13,11 +13,11 @@
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="6.2.1" />
<PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " />
<PackageReference Include="MicroCom.CodeGenerator" Version="0.10.4" />
<PackageReference Include="MicroCom.CodeGenerator" Version="0.11.0" />
<!-- Keep in sync with Avalonia.Build.Tasks -->
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
<PackageReference Include="SourceLink" Version="1.1.0" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Build.Framework" Version="17.3.1" PrivateAssets="All" />
<PackageReference Include="SourceLink" Version="1.1.0" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Build.Framework" Version="17.3.2" PrivateAssets="All" />
<PackageReference Include="xunit.runner.console" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

2
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

4
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -118,7 +118,7 @@
</StackPanel>
</Border>
<Border>
<RichTextBlock SelectionBrush="LightBlue" IsTextSelectionEnabled="True" Margin="10" TextWrapping="Wrap">
<SelectableTextBlock SelectionBrush="LightBlue" Margin="10" TextWrapping="Wrap">
This <Span FontWeight="Bold">is</Span> a
<Span Background="Silver" Foreground="Maroon">TextBlock</Span>
with <Span TextDecorations="Underline">several</Span>
@ -126,7 +126,7 @@
<Span Foreground="Blue">
using a <Bold>variety</Bold> of <Italic>styles</Italic>
</Span>.
</RichTextBlock>
</SelectableTextBlock>
</Border>
</WrapPanel>
</StackPanel>

17
samples/IntegrationTestApp/MainWindow.axaml

@ -69,6 +69,23 @@
</StackPanel>
</TabItem>
<TabItem Header="Gestures">
<DockPanel>
<DockPanel DockPanel.Dock="Top">
<Button Name="ResetGestures" DockPanel.Dock="Right">Reset</Button>
<TextBlock Name="LastGesture" />
</DockPanel>
<Panel>
<Border Name="GestureBorder" Background="Blue"
AutomationProperties.AccessibilityView="Content"
AutomationProperties.ControlTypeOverride="Image"/>
<Border Name="GestureBorder2" Background="Green" IsVisible="False"
AutomationProperties.AccessibilityView="Content"
AutomationProperties.ControlTypeOverride="Image"/>
</Panel>
</DockPanel>
</TabItem>
<TabItem Header="ListBox">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom">

34
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<Border>("GestureBorder");
var gestureBorder2 = this.GetControl<Border>("GestureBorder2");
var lastGesture = this.GetControl<TextBlock>("LastGesture");
var resetGestures = this.GetControl<Button>("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<TextBlock>("ClickedMenuItem");

1
samples/MobileSandbox/MainView.xaml

@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Margin="100 50" Spacing="50">
<TextBlock Text="Login" Foreground="White" />
<TextBox Watermark="Text" />
<TextBox Watermark="Username" TextInputOptions.ContentType="Email" AcceptsReturn="True" TextInputOptions.ReturnKeyType="Search" />
<TextBox Watermark="Password" PasswordChar="*" TextInputOptions.ContentType="Password" />
<TextBox Watermark="Pin" PasswordChar="*" TextInputOptions.ContentType="Digits" />

46
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;
}
}

14
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; }
/// <inheritdoc cref="IPlatformSettings.TouchDoubleClickSize"/>
public Size TouchDoubleClickSize => new Size(4, 4);
/// <inheritdoc cref="IPlatformSettings.TouchDoubleClickTime"/>
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<ICursorFactory>().ToTransient<CursorFactory>()
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToConstant(Instance)
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())

114
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);
}
}
}

2
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()
);

6
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)

16
src/Avalonia.Base/Controls/NameScopeEventArgs.cs

@ -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; }
}
}

2
src/Avalonia.Base/Input/DragEventArgs.cs

@ -32,7 +32,7 @@ namespace Avalonia.Input
return point;
}
public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
internal DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
: base(routedEvent)
{
Data = data;

25
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<PointerDeltaEventArgs>(
"PointerSwipeGesture", RoutingStrategies.Bubble, typeof(Gestures));
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
private static readonly WeakReference<IInteractive> s_lastPress = new WeakReference<IInteractive>(null);
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
private static readonly WeakReference<IInteractive?> s_lastPress = new WeakReference<IInteractive?>(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<IPlatformSettings>();
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)
{

5
src/Avalonia.Base/Input/GotFocusEventArgs.cs

@ -7,6 +7,11 @@ namespace Avalonia.Input
/// </summary>
public class GotFocusEventArgs : RoutedEventArgs
{
internal GotFocusEventArgs()
{
}
/// <summary>
/// Gets or sets a value indicating how the change in focus occurred.
/// </summary>

5
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; }

6
src/Avalonia.Base/Input/MouseDevice.cs

@ -127,9 +127,9 @@ namespace Avalonia.Input
_pointer.Capture(source);
if (source != null)
{
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500;
var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4);
var settings = AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>();
var doubleClickTime = settings.GetDoubleTapTime(PointerType.Mouse).TotalMilliseconds;
var doubleClickSize = settings.GetDoubleTapSize(PointerType.Mouse);
if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime)
{

5
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<IPlatformSettings>();
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)
{

2
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,

12
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -13,7 +13,7 @@ namespace Avalonia.Input
private readonly PointerPointProperties _properties;
private readonly Lazy<IReadOnlyList<RawPointerPoint>?>? _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;

2
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,

6
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;
}

2
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;

1
src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs

@ -29,6 +29,7 @@ namespace Avalonia.Input.TextInput
/// Sets the non-committed input string
/// </summary>
void SetPreeditText(string? text);
/// <summary>
/// Indicates if text input client is capable of providing the text around the cursor
/// </summary>

4
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; }

7
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<IPlatformSettings>();
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,

5
src/Avalonia.Base/Input/VectorEventArgs.cs

@ -5,6 +5,11 @@ namespace Avalonia.Input
{
public class VectorEventArgs : RoutedEventArgs
{
internal VectorEventArgs()
{
}
public Vector Vector { get; set; }
}
}

30
src/Avalonia.Base/Interactivity/RoutedEventArgs.cs

@ -2,29 +2,59 @@ using System;
namespace Avalonia.Interactivity
{
/// <summary>
/// Provides state information and data specific to a routed event.
/// </summary>
public class RoutedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="RoutedEventArgs"/> class.
/// </summary>
public RoutedEventArgs()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RoutedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
public RoutedEventArgs(RoutedEvent? routedEvent)
{
RoutedEvent = routedEvent;
}
/// <summary>
/// Initializes a new instance of the <see cref="RoutedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
/// <param name="source">The source object that raised the routed event.</param>
public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
{
RoutedEvent = routedEvent;
Source = source;
}
/// <summary>
/// Gets or sets a value indicating whether the routed event has already been handled.
/// </summary>
/// <remarks>
/// Once handled, a routed event should be ignored.
/// </remarks>
public bool Handled { get; set; }
/// <summary>
/// Gets or sets the routed event associated with these event args.
/// </summary>
public RoutedEvent? RoutedEvent { get; set; }
/// <summary>
/// Gets or sets the routing strategy (direct, bubbling, or tunneling) of the routed event.
/// </summary>
public RoutingStrategies Route { get; set; }
/// <summary>
/// Gets or sets the source object that raised the routed event.
/// </summary>
public IInteractive? Source { get; set; }
}
}

2
src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs

@ -7,7 +7,7 @@ namespace Avalonia.Layout
/// </summary>
public class EffectiveViewportChangedEventArgs : EventArgs
{
public EffectiveViewportChangedEventArgs(Rect effectiveViewport)
internal EffectiveViewportChangedEventArgs(Rect effectiveViewport)
{
EffectiveViewport = effectiveViewport;
}

6
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)

3
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();

77
src/Avalonia.Base/Media/GlyphRun.cs

@ -170,7 +170,7 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets the scale of the current <see cref="Media.GlyphTypeface"/>
/// Gets the scale of the current <see cref="IGlyphTypeface"/>
/// </summary>
internal double Scale => FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight;
@ -860,82 +860,9 @@ namespace Avalonia.Media
private IGlyphRunImpl CreateGlyphRunImpl()
{
IGlyphRunImpl glyphRunImpl;
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
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()

18
src/Avalonia.Base/Media/Imaging/Bitmap.cs

@ -121,18 +121,28 @@ namespace Avalonia.Media.Imaging
/// Saves the bitmap to a file.
/// </summary>
/// <param name="fileName">The filename.</param>
public void Save(string fileName)
/// <param name="quality">
/// The optional quality for compression.
/// The quality value is interpreted from 0 - 100. If quality is null the default quality
/// setting is applied.
/// </param>
public void Save(string fileName, int? quality = null)
{
PlatformImpl.Item.Save(fileName);
PlatformImpl.Item.Save(fileName, quality);
}
/// <summary>
/// Saves the bitmap to a stream.
/// </summary>
/// <param name="stream">The stream.</param>
public void Save(Stream stream)
/// <param name="quality">
/// The optional quality for compression.
/// The quality value is interpreted from 0 - 100. If quality is null the default quality
/// setting is applied.
/// </param>
public void Save(Stream stream, int? quality = null)
{
PlatformImpl.Item.Save(stream);
PlatformImpl.Item.Save(stream, quality);
}
/// <inheritdoc/>

14
src/Avalonia.Base/Media/Imaging/IBitmap.cs

@ -35,12 +35,22 @@ namespace Avalonia.Media.Imaging
/// Saves the bitmap to a file.
/// </summary>
/// <param name="fileName">The filename.</param>
void Save(string fileName);
/// <param name="quality">
/// 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.
/// </param>
void Save(string fileName, int? quality = null);
/// <summary>
/// Saves the bitmap to a stream in png format.
/// </summary>
/// <param name="stream">The stream.</param>
void Save(Stream stream);
/// <param name="quality">
/// 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.
/// </param>
void Save(Stream stream, int? quality = null);
}
}

12
src/Avalonia.Base/Media/PathMarkupParser.cs

@ -188,7 +188,11 @@ namespace Avalonia.Media
_isOpen = true;
}
private void SetFillRule(scoped ref ReadOnlySpan<char> span)
private void SetFillRule(
#if NET7SDK
scoped
#endif
ref ReadOnlySpan<char> 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<char> remaining, out ReadOnlySpan<char> argument)
private static bool ReadArgument(
#if NET7SDK
scoped
#endif
ref ReadOnlySpan<char> remaining, out ReadOnlySpan<char> argument)
{
remaining = SkipWhitespace(remaining);
if (remaining.IsEmpty)

2
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@ -140,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
}
}
return length;
return Math.Min(length, text.Length);
}
}
}

19
src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs

@ -17,15 +17,18 @@
/// <param name="textAlignment">logical horizontal alignment</param>
/// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param>
/// <param name="letterSpacing">letter spacing</param>
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;
}
/// <summary>
@ -39,6 +42,7 @@
/// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param>
/// <param name="indent">line indentation</param>
/// <param name="letterSpacing">letter spacing</param>
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 @@
/// </summary>
public override double Indent { get; }
/// <summary>
/// The letter spacing
/// </summary>
public override double LetterSpacing { get; }
/// <summary>
/// Set text flow direction
/// </summary>

7
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;
}

21
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -31,6 +31,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <param name="maxLines">The maximum number of text lines.</param>
/// <param name="textStyleOverrides">The text style overrides.</param>
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<ValueSpan<TextRunProperties>>? 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
/// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <param name="maxLines">The maximum number of text lines.</param>
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
/// </summary>
public int MaxLines { get; }
/// <summary>
/// Gets the text spacing.
/// </summary>
public double LetterSpacing { get; }
/// <summary>
/// Gets the text lines.
/// </summary>
@ -374,15 +387,17 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textDecorations">The text decorations.</param>
/// <param name="flowDirection">The text flow direction.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <returns></returns>
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);
}
/// <summary>

9
src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs

@ -57,7 +57,7 @@
public abstract double Indent { get; }
/// <summary>
/// Paragraph indentation
/// Get the paragraph indentation.
/// </summary>
public virtual double ParagraphIndent
{
@ -65,11 +65,16 @@
}
/// <summary>
/// Default Incremental Tab
/// Gets the default incremental tab width.
/// </summary>
public virtual double DefaultIncrementalTab
{
get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; }
}
/// <summary>
/// Gets the letter spacing.
/// </summary>
public virtual double LetterSpacing { get; }
}
}

9
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;
}
/// <summary>
@ -45,5 +47,10 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public double IncrementalTabWidth { get; }
/// <summary>
/// Get the letter spacing.
/// </summary>
public double LetterSpacing { get; }
}
}

30
src/Avalonia.Base/Platform/DefaultPlatformSettings.cs

@ -0,0 +1,30 @@
using System;
using Avalonia.Input;
namespace Avalonia.Platform
{
/// <summary>
/// A default implementation of <see cref="IPlatformSettings"/> for platforms which don't have
/// an OS-specific implementation.
/// </summary>
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);
}
}

14
src/Avalonia.Base/Platform/IBitmapImpl.cs

@ -29,12 +29,22 @@ namespace Avalonia.Platform
/// Saves the bitmap to a file.
/// </summary>
/// <param name="fileName">The filename.</param>
void Save(string fileName);
/// <param name="quality">
/// 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.
/// </param>
void Save(string fileName, int? quality = null);
/// <summary>
/// Saves the bitmap to a stream in png format.
/// </summary>
/// <param name="stream">The stream.</param>
void Save(Stream stream);
/// <param name="quality">
/// 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.
/// </param>
void Save(Stream stream, int? quality = null);
}
}

37
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);
/// <summary>
/// Allocates a platform glyph run buffer.
/// Creates a platform implementation of a glyph run.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="length">The length.</param>
/// <returns>An <see cref="IGlyphRunBuffer"/>.</returns>
/// <remarks>
/// This buffer only holds glyph indices.
/// </remarks>
IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
/// <summary>
/// Allocates a horizontal platform glyph run buffer.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="length">The length.</param>
/// <returns>An <see cref="IGlyphRunBuffer"/>.</returns>
/// <remarks>
/// This buffer holds glyph indices and glyph advances.
/// </remarks>
IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
/// <summary>
/// Allocates a positioned platform glyph run buffer.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="length">The length.</param>
/// <returns>An <see cref="IGlyphRunBuffer"/>.</returns>
/// <remarks>
/// This buffer holds glyph indices, glyph advances and glyph positions.
/// </remarks>
IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
/// <param name="glyphIndices">The glyph indices.</param>
/// <param name="glyphAdvances">The glyph advances.</param>
/// <param name="glyphOffsets">The glyph offsets.</param>
/// <returns></returns>
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double>? glyphAdvances, IReadOnlyList<Vector>? glyphOffsets);
/// <summary>
/// Gets a value indicating whether the platform directly supports rectangles with rounded corners.

22
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; }
/// <summary>
/// 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.
/// </summary>
/// <param name="type">The pointer type.</param>
Size GetTapSize(PointerType type);
/// <summary>
/// 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.
/// </summary>
Size TouchDoubleClickSize { get; }
/// <param name="type">The pointer type.</param>
Size GetDoubleTapSize(PointerType type);
/// <summary>
/// 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.
/// </summary>
TimeSpan TouchDoubleClickTime { get; }
TimeSpan GetDoubleTapTime(PointerType type);
}
}

2
src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs

@ -12,7 +12,7 @@ namespace Avalonia.Rendering
/// </summary>
/// <param name="root">The render root that has been updated.</param>
/// <param name="dirtyRect">The updated area.</param>
public SceneInvalidatedEventArgs(
internal SceneInvalidatedEventArgs(
IRenderRoot root,
Rect dirtyRect)
{

6
src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -8,7 +8,11 @@ namespace Avalonia.Utilities
#endif
static class IdentifierParser
{
public static ReadOnlySpan<char> ParseIdentifier(this scoped ref CharacterReader r)
public static ReadOnlySpan<char> ParseIdentifier(this
#if NET7SDK
scoped
#endif
ref CharacterReader r)
{
if (IsValidIdentifierStart(r.Peek))
{

83
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

8
src/Avalonia.Controls/Automation/AutomationProperties.cs

@ -9,6 +9,11 @@ namespace Avalonia.Automation
/// </summary>
public enum AccessibilityView
{
/// <summary>
/// The control's view is defined by its automation peer.
/// </summary>
Default,
/// <summary>
/// The control is included in the Raw view of the automation tree.
/// </summary>
@ -44,8 +49,7 @@ namespace Avalonia.Automation
public static readonly AttachedProperty<AccessibilityView> AccessibilityViewProperty =
AvaloniaProperty.RegisterAttached<StyledElement, AccessibilityView>(
"AccessibilityView",
typeof(AutomationProperties),
defaultValue: AccessibilityView.Content);
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.AccessKey attached property

14
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.
/// </summary>
public bool IsContentElement() => IsControlElement() && IsContentElementCore();
public bool IsContentElement() => IsContentElementOverrideCore();
/// <summary>
/// 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.
/// </summary>
public bool IsControlElement() => IsControlElementCore();
public bool IsControlElement() => IsControlElementOverrideCore();
/// <summary>
/// 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;

16
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();

41
src/Avalonia.Controls/Control.cs

@ -84,6 +84,13 @@ namespace Avalonia.Controls
nameof(Unloaded),
RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="SizeChanged"/> event.
/// </summary>
public static readonly RoutedEvent<SizeChangedEventArgs> SizeChangedEvent =
RoutedEvent.Register<Control, SizeChangedEventArgs>(
nameof(SizeChanged), RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="FlowDirection"/> property.
/// </summary>
@ -211,6 +218,15 @@ namespace Avalonia.Controls
remove => RemoveHandler(UnloadedEvent, value);
}
/// <summary>
/// Occurs when the bounds (actual size) of the control have changed.
/// </summary>
public event EventHandler<SizeChangedEventArgs>? SizeChanged
{
add => AddHandler(SizeChangedEvent, value);
remove => RemoveHandler(SizeChangedEvent, value);
}
public new IControl? Parent => (IControl?)base.Parent;
/// <summary>
@ -530,14 +546,35 @@ namespace Avalonia.Controls
}
}
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == FlowDirectionProperty)
if (change.Property == BoundsProperty)
{
var oldValue = change.GetOldValue<Rect>();
var newValue = change.GetNewValue<Rect>();
// 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)

91
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -12,7 +12,7 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList<Inline>
{
private ILogical? _parent;
private IAvaloniaList<ILogical>? _logicalChildren;
private IInlineHost? _inlineHost;
/// <summary>
@ -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<ILogical>? 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);
}
/// <summary>
/// Add a text segment to the collection.
/// Adds a text segment to the collection.
/// <remarks>
/// For non complex content this appends the text to the end of currently held text.
/// For complex content this adds a <see cref="Run"/> to the collection.
/// </remarks>
/// </summary>
/// <param name="text"></param>
/// <param name="text">The to be added text.</param>
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()
/// <summary>
/// Adds a control wrapped inside a <see cref="InlineUIContainer"/> to the collection.
/// </summary>
/// <param name="control">The to be added control.</param>
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));
}
/// <summary>
@ -152,20 +148,21 @@ namespace Avalonia.Controls.Documents
Invalidated?.Invoke(this, EventArgs.Empty);
}
private void OnParentChanged(ILogical? parent)
private void OnParentChanged(IAvaloniaList<ILogical>? oldParent, IAvaloniaList<ILogical>? 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);
}
}
}
}

6
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();
}

49
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<double> LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner<TextPresenter>();
/// <summary>
/// Defines the <see cref="LetterSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> LetterSpacingProperty =
TextBlock.LetterSpacingProperty.AddOwner<TextPresenter>();
/// <summary>
/// Defines the <see cref="Background"/> property.
/// </summary>
@ -212,6 +219,15 @@ namespace Avalonia.Controls.Presenters
set => SetValue(LineHeightProperty, value);
}
/// <summary>
/// Gets or sets the letter spacing.
/// </summary>
public double LetterSpacing
{
get => GetValue(LetterSpacingProperty);
set => SetValue(LetterSpacingProperty, value);
}
/// <summary>
/// Gets or sets the text alignment.
/// </summary>
@ -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):

19
src/Avalonia.Controls/ProgressBar.cs

@ -110,7 +110,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty<string> ProgressTextFormatProperty =
AvaloniaProperty.Register<ProgressBar, string>(nameof(ProgressTextFormat), "{1:0}%");
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<ProgressBar, Orientation>(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<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
MaximumProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
OrientationProperty.Changed.AddClassHandler<ProgressBar>((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<Border>("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;
}

374
src/Avalonia.Controls/RichTextBlock.cs → 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
/// <summary>
/// A control that displays a block of formatted text.
/// </summary>
public class RichTextBlock : TextBlock, IInlineHost
public class SelectableTextBlock : TextBlock, IInlineHost
{
public static readonly StyledProperty<bool> IsTextSelectionEnabledProperty =
AvaloniaProperty.Register<RichTextBlock, bool>(nameof(IsTextSelectionEnabled), false);
public static readonly DirectProperty<RichTextBlock, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
public static readonly DirectProperty<SelectableTextBlock, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
nameof(SelectionStart),
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly DirectProperty<RichTextBlock, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
public static readonly DirectProperty<SelectableTextBlock, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly DirectProperty<RichTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, string>(
public static readonly DirectProperty<SelectableTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, string>(
nameof(SelectedText),
o => o.SelectedText);
public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue);
AvaloniaProperty.Register<SelectableTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue);
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly StyledProperty<InlineCollection?> InlinesProperty =
AvaloniaProperty.Register<RichTextBlock, InlineCollection?>(
nameof(Inlines));
public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
public static readonly DirectProperty<SelectableTextBlock, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<SelectableTextBlock, bool>(
nameof(CanCopy),
o => o.CanCopy);
public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
RoutedEvent.Register<RichTextBlock, RoutedEventArgs>(
RoutedEvent.Register<SelectableTextBlock, RoutedEventArgs>(
nameof(CopyingToClipboard), RoutingStrategies.Bubble);
private bool _canCopy;
private int _selectionStart;
private int _selectionEnd;
private int _wordSelectionStart = -1;
private IReadOnlyList<TextRun>? _textRuns;
static RichTextBlock()
static SelectableTextBlock()
{
FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
AffectsRender<RichTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty);
FocusableProperty.OverrideDefaultValue(typeof(SelectableTextBlock), true);
AffectsRender<SelectableTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty);
}
public RichTextBlock()
public event EventHandler<RoutedEventArgs>? CopyingToClipboard
{
Inlines = new InlineCollection
{
Parent = this,
InlineHost = this
};
add => AddHandler(CopyingToClipboardEvent, value);
remove => RemoveHandler(CopyingToClipboardEvent, value);
}
/// <summary>
@ -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();
}
/// <summary>
/// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API.
/// </summary>
public bool IsTextSelectionEnabled
{
get => GetValue(IsTextSelectionEnabledProperty);
set => SetValue(IsTextSelectionEnabledProperty, value);
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection? Inlines
{
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
/// <summary>
/// Property for determining if the Copy command can be executed.
/// </summary>
@ -154,20 +124,12 @@ namespace Avalonia.Controls
private set => SetAndRaise(CanCopyProperty, ref _canCopy, value);
}
public event EventHandler<RoutedEventArgs>? CopyingToClipboard
{
add => AddHandler(CopyingToClipboardEvent, value);
remove => RemoveHandler(CopyingToClipboardEvent, value);
}
internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
/// <summary>
/// Copies the current selection to the Clipboard.
/// </summary>
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);
}
}
/// <summary>
/// Select all text in the TextBox
/// </summary>
public void SelectAll()
{
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text;
SelectionStart = 0;
@ -238,94 +168,52 @@ namespace Avalonia.Controls
/// </summary>
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();
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <returns>A <see cref="TextLayout"/> object.</returns>
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<TextRun>();
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<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> 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;
}
}
}
}

83
src/Avalonia.Controls/SizeChangedEventArgs.cs

@ -0,0 +1,83 @@
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data specific to a SizeChanged event.
/// </summary>
public class SizeChangedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="SizeChangedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
public SizeChangedEventArgs(RoutedEvent? routedEvent)
: base (routedEvent)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SizeChangedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
/// <param name="source">The source object that raised the routed event.</param>
public SizeChangedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
: base(routedEvent, source)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SizeChangedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
/// <param name="source">The source object that raised the routed event.</param>
/// <param name="previousSize">The previous size (or bounds) of the object.</param>
/// <param name="newSize">The new size (or bounds) of the object.</param>
public SizeChangedEventArgs(
RoutedEvent? routedEvent,
IInteractive? source,
Size previousSize,
Size newSize)
: base(routedEvent, source)
{
PreviousSize = previousSize;
NewSize = newSize;
}
/// <summary>
/// Gets a value indicating whether the height of the new size is considered
/// different than the previous size height.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public bool HeightChanged => !MathUtilities.AreClose(NewSize.Height, PreviousSize.Height, LayoutHelper.LayoutEpsilon);
/// <summary>
/// Gets the new size (or bounds) of the object.
/// </summary>
public Size NewSize { get; init; }
/// <summary>
/// Gets the previous size (or bounds) of the object.
/// </summary>
public Size PreviousSize { get; init; }
/// <summary>
/// Gets a value indicating whether the width of the new size is considered
/// different than the previous size width.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public bool WidthChanged => !MathUtilities.AreClose(NewSize.Width, PreviousSize.Width, LayoutHelper.LayoutEpsilon);
}
}

312
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
/// <summary>
/// A control that displays a block of text.
/// </summary>
public class TextBlock : Control, IAddChild<string>
public class TextBlock : Control, IInlineHost
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -81,6 +82,15 @@ namespace Avalonia.Controls
validate: IsValidLineHeight,
inherits: true);
/// <summary>
/// Defines the <see cref="LetterSpacing"/> property.
/// </summary>
public static readonly AttachedProperty<double> LetterSpacingProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, double>(
nameof(LetterSpacing),
0,
inherits: true);
/// <summary>
/// Defines the <see cref="MaxLines"/> property.
/// </summary>
@ -96,15 +106,15 @@ namespace Avalonia.Controls
public static readonly DirectProperty<TextBlock, string?> TextProperty =
AvaloniaProperty.RegisterDirect<TextBlock, string?>(
nameof(Text),
o => o.Text,
(o, v) => o.Text = v);
o => o.GetText(),
(o, v) => o.SetText(v));
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
/// </summary>
public static readonly AttachedProperty<TextAlignment> TextAlignmentProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextAlignment>(
nameof(TextAlignment),
nameof(TextAlignment),
defaultValue: TextAlignment.Start,
inherits: true);
@ -112,14 +122,14 @@ namespace Avalonia.Controls
/// Defines the <see cref="TextWrapping"/> property.
/// </summary>
public static readonly AttachedProperty<TextWrapping> TextWrappingProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextWrapping>(nameof(TextWrapping),
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextWrapping>(nameof(TextWrapping),
inherits: true);
/// <summary>
/// Defines the <see cref="TextTrimming"/> property.
/// </summary>
public static readonly AttachedProperty<TextTrimming> TextTrimmingProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextTrimming>(nameof(TextTrimming),
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextTrimming>(nameof(TextTrimming),
defaultValue: TextTrimming.None,
inherits: true);
@ -129,9 +139,17 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations));
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly StyledProperty<InlineCollection?> InlinesProperty =
AvaloniaProperty.Register<TextBlock, InlineCollection?>(
nameof(Inlines));
internal string? _text;
protected TextLayout? _textLayout;
protected Size _constraint;
private IReadOnlyList<TextRun>? _textRuns;
/// <summary>
/// Initializes static members of the <see cref="TextBlock"/> class.
@ -139,10 +157,19 @@ namespace Avalonia.Controls
static TextBlock()
{
ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty);
}
public TextBlock()
{
Inlines = new InlineCollection
{
LogicalChildren = LogicalChildren,
InlineHost = this
};
}
/// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary>
@ -244,6 +271,15 @@ namespace Avalonia.Controls
set => SetValue(LineHeightProperty, value);
}
/// <summary>
/// Gets or sets the letter spacing.
/// </summary>
public double LetterSpacing
{
get => GetValue(LetterSpacingProperty);
set => SetValue(LetterSpacingProperty, value);
}
/// <summary>
/// Gets or sets the maximum number of text lines.
/// </summary>
@ -288,9 +324,21 @@ namespace Avalonia.Controls
get => GetValue(TextDecorationsProperty);
set => SetValue(TextDecorationsProperty, value);
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection? Inlines
{
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
protected override bool BypassFlowDirectionPolicies => true;
internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
/// <summary>
/// The BaselineOffset property provides an adjustment to baseline offset
/// </summary>
@ -445,6 +493,35 @@ namespace Avalonia.Controls
control.SetValue(LineHeightProperty, height);
}
/// <summary>
/// Reads the attached property from the given element
/// </summary>
/// <param name="control">The element to which to read the attached property.</param>
public static double GetLetterSpacing(Control control)
{
if (control == null)
{
throw new ArgumentNullException(nameof(control));
}
return control.GetValue(LetterSpacingProperty);
}
/// <summary>
/// Writes the attached property LetterSpacing to the given element.
/// </summary>
/// <param name="control">The element to which to write the attached property.</param>
/// <param name="letterSpacing">The property value to set</param>
public static void SetLetterSpacing(Control control, double letterSpacing)
{
if (control == null)
{
throw new ArgumentNullException(nameof(control));
}
control.SetValue(LetterSpacingProperty, letterSpacing);
}
/// <summary>
/// Reads the attached property from the given element
/// </summary>
@ -513,19 +590,19 @@ namespace Avalonia.Controls
TextLayout.Draw(context, origin);
}
void IAddChild<string>.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);
}
/// <summary>
@ -534,17 +611,30 @@ namespace Avalonia.Controls
/// <returns>A <see cref="TextLayout"/> object.</returns>
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<TextRun>();
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<char> _text;
@ -674,5 +855,46 @@ namespace Avalonia.Controls
return new TextCharacters(runText, _defaultProperties);
}
}
private readonly struct InlinesTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
{
_textRuns = textRuns;
}
public IReadOnlyList<TextRun> 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;
}
}
}
}

14
src/Avalonia.Controls/TextBox.cs

@ -114,6 +114,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner<TextBox>();
/// <summary>
/// Defines see <see cref="TextBlock.LetterSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> LetterSpacingProperty =
TextBlock.LetterSpacingProperty.AddOwner<TextBox>();
public static readonly StyledProperty<string?> WatermarkProperty =
AvaloniaProperty.Register<TextBox, string?>(nameof(Watermark));
@ -378,6 +384,12 @@ namespace Avalonia.Controls
set => SetValue(MaxLinesProperty, value);
}
public double LetterSpacing
{
get => GetValue(LetterSpacingProperty);
set => SetValue(LetterSpacingProperty, value);
}
/// <summary>
/// Gets or sets the line height.
/// </summary>
@ -1200,7 +1212,7 @@ namespace Avalonia.Controls
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (_presenter == null || !string.IsNullOrEmpty(_presenter.PreeditText))
if (_presenter == null )
{
return;
}

11
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<IClipboard>().ToSingleton<ClipboardStub>()
.Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(Keyboard)
.Bind<IPlatformSettings>().ToConstant(instance)
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(threading)
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
@ -60,12 +60,5 @@ namespace Avalonia.DesignerSupport.Remote
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
}
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;
}
}

2
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -63,7 +63,7 @@ namespace Avalonia.Headless
.Bind<IPlatformThreadingInterface>().ToConstant(new HeadlessPlatformThreadingInterface())
.Bind<IClipboard>().ToSingleton<HeadlessClipboardStub>()
.Bind<ICursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()
.Bind<IPlatformSettings>().ToConstant(new HeadlessPlatformSettingsStub())
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IPlatformIconLoader>().ToSingleton<HeadlessIconLoaderStub>()
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
.Bind<IRenderLoop>().ToConstant(new RenderLoop())

46
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<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> 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<ushort> GlyphIndices => Span<ushort>.Empty;
public IGlyphRunImpl Build()
{
return new HeadlessGlyphRunStub();
}
}
class HeadlessHorizontalGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IHorizontalGlyphRunBuffer
{
public Span<float> GlyphPositions => Span<float>.Empty;
}
class HeadlessPositionedGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IPositionedGlyphRunBuffer
{
public Span<System.Drawing.PointF> GlyphPositions => Span<System.Drawing.PointF>.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)
{
}

10
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

2
src/Avalonia.MicroCom/Avalonia.MicroCom.csproj

@ -4,7 +4,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MicroCom.Runtime" Version="0.10.5" />
<PackageReference Include="MicroCom.Runtime" Version="0.11.0" />
<ProjectReference Include="..\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<ExcludeAssets>all</ExcludeAssets>

2
src/Avalonia.Native/Avalonia.Native.csproj

@ -24,7 +24,7 @@
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<PackageReference Include="MicroCom.CodeGenerator.MSBuild" Version="0.10.4" PrivateAssets="all" />
<PackageReference Include="MicroCom.CodeGenerator.MSBuild" Version="0.11.0" PrivateAssets="all" />
<MicroComIdl Include="avn.idl" CSharpInteropPath="Interop.Generated.cs" />
</ItemGroup>

16
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
/// <inheritdoc cref="IPlatformSettings.TouchDoubleClickSize"/>
public Size TouchDoubleClickSize => new Size(16, 16);
/// <inheritdoc cref="IPlatformSettings.TouchDoubleClickTime"/>
public TimeSpan TouchDoubleClickTime => DoubleClickTime;
public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options)
{
var result = new AvaloniaNativePlatform(MicroComRuntime.CreateProxyFor<IAvaloniaNativeFactory>(factory, true));
@ -103,6 +93,8 @@ namespace Avalonia.Native
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>() ?? 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<ICursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
.Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IPlatformSettings>().ToConstant(this)
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IWindowingPlatform>().ToConstant(this)
.Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))

8
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.
/// </summary>
public bool DisableNativeMenus { get; set; }
public bool DisableSetProcessName { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool DisableAvaloniaAppDelegate { get; set; }
}
}

6
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<IPlatformSettings>();
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;
}

2
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)]

2
src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml

@ -68,7 +68,7 @@
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/Slider.xaml" />
<!-- ManagedFileChooser comes last because it uses (and overrides) styles for a multitude of other controls...the dialogs were originally UserControls, after all -->
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml"/>
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>

14
src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml

@ -1,14 +0,0 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<RichTextBlock IsTextSelectionEnabled="True" Text="Preview"/>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type RichTextBlock}" TargetType="RichTextBlock">
<Style Selector="^[IsTextSelectionEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
</Style>
</ControlTheme>
</ResourceDictionary>

18
src/Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml

@ -0,0 +1,18 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<SelectableTextBlock Text="Preview" />
</Design.PreviewWith>
<MenuFlyout x:Key="SelectableTextBlockContextFlyout" Placement="Bottom">
<MenuItem x:Name="SelectableTextBlockContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[SelectableTextBlock].Copy}"
IsEnabled="{Binding $parent[SelectableTextBlock].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}" />
</MenuFlyout>
<ControlTheme x:Key="{x:Type SelectableTextBlock}" TargetType="SelectableTextBlock">
<Style Selector="^[IsEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
<Setter Property="ContextFlyout" Value="{StaticResource SelectableTextBlockContextFlyout}" />
</Style>
</ControlTheme>
</ResourceDictionary>

7
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 @@
<Setter Property="InnerRightContent">
<Template>
<ToggleButton Theme="{StaticResource FluentTextBoxToggleButton}"
IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}">
IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}"
ClipToBounds="True">
<Panel>
<PathIcon Data="{StaticResource PasswordBoxRevealButtonData}"
Height="8" Width="12"
@ -252,7 +254,8 @@
<Setter Property="InnerRightContent">
<Template>
<Button Theme="{StaticResource FluentTextBoxButton}"
Command="{Binding $parent[TextBox].Clear}">
Command="{Binding $parent[TextBox].Clear}"
ClipToBounds="True">
<PathIcon Data="{StaticResource TextBoxClearButtonData}" Height="10" Width="10"/>
</Button>
</Template>

14
src/Avalonia.Themes.Simple/Controls/RichTextBlock.xaml

@ -1,14 +0,0 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<RichTextBlock IsTextSelectionEnabled="True"
Text="Preview" />
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type RichTextBlock}"
TargetType="RichTextBlock">
<Style Selector="^[IsTextSelectionEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
</Style>
</ControlTheme>
</ResourceDictionary>

18
src/Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml

@ -0,0 +1,18 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<SelectableTextBlock Text="Preview" />
</Design.PreviewWith>
<MenuFlyout x:Key="SelectableTextBlockContextFlyout" Placement="Bottom">
<MenuItem x:Name="SelectableTextBlockContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[SelectableTextBlock].Copy}"
IsEnabled="{Binding $parent[SelectableTextBlock].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}" />
</MenuFlyout>
<ControlTheme x:Key="{x:Type SelectableTextBlock}" TargetType="SelectableTextBlock">
<Style Selector="^[IsEnabled=True]">
<Setter Property="Cursor" Value="IBeam" />
<Setter Property="ContextFlyout" Value="{StaticResource SelectableTextBlockContextFlyout}" />
</Style>
</ControlTheme>
</ResourceDictionary>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save