Browse Source

Merge branch 'master' into features/Linux/Framebuffer/LibInput/Pointer

pull/9069/head
Max Katz 3 years ago
committed by GitHub
parent
commit
6081140e52
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .editorconfig
  2. 1
      .github/FUNDING.yml
  3. 60
      Avalonia.Desktop.slnf
  4. 1
      Avalonia.sln
  5. 5
      Directory.Build.targets
  6. 38
      Documentation/build.md
  7. 15
      azure-pipelines-integrationtests.yml
  8. 12
      azure-pipelines.yml
  9. 2
      build/SharedVersion.props
  10. 4
      global.json
  11. 4
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  12. 4
      native/Avalonia.Native/src/OSX/Screens.mm
  13. 4
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  14. 11
      native/Avalonia.Native/src/OSX/app.mm
  15. 2
      native/Avalonia.Native/src/OSX/common.h
  16. 41
      native/Avalonia.Native/src/OSX/main.mm
  17. 5
      native/Avalonia.Native/src/OSX/rendertarget.mm
  18. 19
      nukebuild/Build.cs
  19. 6
      nukebuild/_build.csproj
  20. 3
      packages/Avalonia/AvaloniaBuildTasks.targets
  21. 2
      readme.md
  22. 1
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  23. 5
      samples/ControlCatalog.Web/ControlCatalog.Web.csproj
  24. 1
      samples/ControlCatalog.Web/Roots.xml
  25. 21
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  26. 1
      samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
  27. 17
      samples/ControlCatalog/Pages/ExpanderPage.xaml
  28. 4
      samples/ControlCatalog/Pages/ScreenPage.cs
  29. 4
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  30. 2
      src/Avalonia.Base/Avalonia.Base.csproj
  31. 6
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  32. 16
      src/Avalonia.Base/Controls/NameScopeEventArgs.cs
  33. 2
      src/Avalonia.Base/Input/DragDrop.cs
  34. 8
      src/Avalonia.Base/Input/DragDropDevice.cs
  35. 2
      src/Avalonia.Base/Input/DragEventArgs.cs
  36. 5
      src/Avalonia.Base/Input/GotFocusEventArgs.cs
  37. 5
      src/Avalonia.Base/Input/KeyEventArgs.cs
  38. 2
      src/Avalonia.Base/Input/PointerDeltaEventArgs.cs
  39. 12
      src/Avalonia.Base/Input/PointerEventArgs.cs
  40. 2
      src/Avalonia.Base/Input/PointerWheelEventArgs.cs
  41. 6
      src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
  42. 2
      src/Avalonia.Base/Input/TappedEventArgs.cs
  43. 4
      src/Avalonia.Base/Input/TextInputEventArgs.cs
  44. 5
      src/Avalonia.Base/Input/VectorEventArgs.cs
  45. 30
      src/Avalonia.Base/Interactivity/RoutedEventArgs.cs
  46. 2
      src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs
  47. 6
      src/Avalonia.Base/Layout/UniformGridLayoutState.cs
  48. 49
      src/Avalonia.Base/Media/Color.cs
  49. 27
      src/Avalonia.Base/Media/FontSimulations.cs
  50. 3
      src/Avalonia.Base/Media/FormattedText.cs
  51. 24
      src/Avalonia.Base/Media/GlyphMetrics.cs
  52. 77
      src/Avalonia.Base/Media/GlyphRun.cs
  53. 30
      src/Avalonia.Base/Media/HslColor.cs
  54. 30
      src/Avalonia.Base/Media/HsvColor.cs
  55. 15
      src/Avalonia.Base/Media/IGlyphTypeface.cs
  56. 18
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  57. 14
      src/Avalonia.Base/Media/Imaging/IBitmap.cs
  58. 12
      src/Avalonia.Base/Media/PathMarkupParser.cs
  59. 19
      src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
  60. 36
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  61. 21
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  62. 9
      src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
  63. 9
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  64. 14
      src/Avalonia.Base/Platform/IBitmapImpl.cs
  65. 37
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  66. 2
      src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs
  67. 6
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  68. 49
      src/Avalonia.Base/Utilities/SpanHelpers.cs
  69. 5
      src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs
  70. 6
      src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
  71. 3
      src/Avalonia.Build.Tasks/Program.cs
  72. 72
      src/Avalonia.Build.Tasks/SpanCompat.cs
  73. 32
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs
  74. 17
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  75. 83
      src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs
  76. 28
      src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs
  77. 2
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
  78. 484
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  79. 486
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  80. 4
      src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs
  81. 4
      src/Avalonia.Controls/AppBuilderBase.cs
  82. 41
      src/Avalonia.Controls/Control.cs
  83. 2
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  84. 2
      src/Avalonia.Controls/DefinitionBase.cs
  85. 91
      src/Avalonia.Controls/Documents/InlineCollection.cs
  86. 6
      src/Avalonia.Controls/Documents/Span.cs
  87. 3
      src/Avalonia.Controls/ListBox.cs
  88. 6
      src/Avalonia.Controls/Platform/IScreenImpl.cs
  89. 57
      src/Avalonia.Controls/Platform/Screen.cs
  90. 20
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  91. 45
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  92. 19
      src/Avalonia.Controls/ProgressBar.cs
  93. 23
      src/Avalonia.Controls/Screens.cs
  94. 374
      src/Avalonia.Controls/SelectableTextBlock.cs
  95. 83
      src/Avalonia.Controls/SizeChangedEventArgs.cs
  96. 312
      src/Avalonia.Controls/TextBlock.cs
  97. 12
      src/Avalonia.Controls/TextBox.cs
  98. 30
      src/Avalonia.Controls/TreeViewItem.cs
  99. 1
      src/Avalonia.Controls/Viewbox.cs
  100. 2
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/SimpleWebSocketHttpServer.cs

6
.editorconfig

@ -139,10 +139,12 @@ dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomme
# CA1802: Use literals where appropriate # CA1802: Use literals where appropriate
dotnet_diagnostic.CA1802.severity = warning dotnet_diagnostic.CA1802.severity = warning
# CA1825: Avoid zero-length array allocations # CA1820: Test for empty strings using string length
dotnet_diagnostic.CA1825.severity = warning dotnet_diagnostic.CA1820.severity = warning
# CA1821: Remove empty finalizers # CA1821: Remove empty finalizers
dotnet_diagnostic.CA1821.severity = warning dotnet_diagnostic.CA1821.severity = warning
# CA1825: Avoid zero-length array allocations
dotnet_diagnostic.CA1825.severity = warning
#CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters #CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
dotnet_diagnostic.CA1847.severity = warning dotnet_diagnostic.CA1847.severity = warning

1
.github/FUNDING.yml

@ -1 +1,2 @@
github: avaloniaui
open_collective: avalonia open_collective: avalonia

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

@ -41,6 +41,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE
src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
src\Avalonia.Base\Compatibility\StringCompatibilityExtensions.cs = src\Avalonia.Base\Compatibility\StringCompatibilityExtensions.cs
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}"

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 # 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 git clone https://github.com/AvaloniaUI/Avalonia.git
@ -10,15 +10,30 @@ cd Avalonia
git submodule update --init 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). 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** * **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. 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. 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. 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 git clone https://github.com/AvaloniaUI/Avalonia.git
@ -52,7 +66,7 @@ cd Avalonia
git submodule update --init --recursive 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. 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.sh CompileNative
``` ```
### Build and Run Avalonia ## Build and Run Avalonia
``` ```
cd samples/ControlCatalog.NetCore cd samples/ControlCatalog.NetCore

15
azure-pipelines-integrationtests.yml

@ -12,6 +12,16 @@ jobs:
name: 'AvaloniaMacPool' name: 'AvaloniaMacPool'
steps: steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401'
inputs:
version: 6.0.401
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100-rc.2.22477.23'
inputs:
version: 7.0.100-rc.2.22477.23
- script: system_profiler SPDisplaysDataType |grep Resolution - script: system_profiler SPDisplaysDataType |grep Resolution
- script: | - script: |
@ -45,6 +55,11 @@ jobs:
inputs: inputs:
version: 6.0.401 version: 6.0.401
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100-rc.2.22477.23'
inputs:
version: 7.0.100-rc.2.22477.23
- task: Windows Application Driver@0 - task: Windows Application Driver@0
inputs: inputs:
OperationType: 'Start' OperationType: 'Start'

12
azure-pipelines.yml

@ -35,9 +35,9 @@ jobs:
version: 6.0.401 version: 6.0.401
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' displayName: 'Use .NET Core SDK 7.0'
inputs: inputs:
version: 7.0.100-rc.1.22431.12 version: 7.0.100
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Workloads' displayName: 'Install Workloads'
@ -72,9 +72,9 @@ jobs:
version: 6.0.401 version: 6.0.401
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' displayName: 'Use .NET Core SDK 7.0.100'
inputs: inputs:
version: 7.0.100-rc.1.22431.12 version: 7.0.100
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Workloads' displayName: 'Install Workloads'
@ -143,9 +143,9 @@ jobs:
version: 6.0.401 version: 6.0.401
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' displayName: 'Use .NET Core SDK 7.0.100'
inputs: inputs:
version: 7.0.100-rc.1.22431.12 version: 7.0.100
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Workloads' displayName: 'Install Workloads'

2
build/SharedVersion.props

@ -8,7 +8,7 @@
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl> <RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591</NoWarn> <NoWarn>CS1591</NoWarn>
<LangVersion>latest</LangVersion> <LangVersion>preview</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Icon.png</PackageIcon> <PackageIcon>Icon.png</PackageIcon>
<PackageDescription>Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.</PackageDescription> <PackageDescription>Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.</PackageDescription>

4
global.json

@ -1,4 +1,8 @@
{ {
"sdk": {
"version": "7.0.100",
"rollForward": "latestFeature"
},
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43", "Microsoft.Build.Traversal": "1.0.43",
"MSBuild.Sdk.Extras": "3.0.22", "MSBuild.Sdk.Extras": "3.0.22",

4
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -385,7 +385,7 @@
return true; return true;
} }
-(void)resignKeyWindow -(void)windowDidResignKey:(NSNotification *)notification
{ {
if(_parent) if(_parent)
_parent->BaseEvents->Deactivated(); _parent->BaseEvents->Deactivated();
@ -393,8 +393,6 @@
[self showAppMenuOnly]; [self showAppMenuOnly];
[self invalidateShadow]; [self invalidateShadow];
[super resignKeyWindow];
} }
- (void)windowDidMove:(NSNotification *_Nonnull)notification - (void)windowDidMove:(NSNotification *_Nonnull)notification

4
native/Avalonia.Native/src/OSX/Screens.mm

@ -41,9 +41,9 @@ public:
ret->WorkingArea.X = [screen visibleFrame].origin.x; ret->WorkingArea.X = [screen visibleFrame].origin.x;
ret->WorkingArea.Y = ConvertPointY(ToAvnPoint([screen visibleFrame].origin)).Y - ret->WorkingArea.Height; ret->WorkingArea.Y = ConvertPointY(ToAvnPoint([screen visibleFrame].origin)).Y - ret->WorkingArea.Height;
ret->PixelDensity = [screen backingScaleFactor]; ret->Scaling = [screen backingScaleFactor];
ret->Primary = index == 0; ret->IsPrimary = index == 0;
return S_OK; return S_OK;
} }

4
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -63,7 +63,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
START_COM_CALL; START_COM_CALL;
@autoreleasepool { @autoreleasepool {
_isDialog = isDialog; _isDialog = isDialog || _parent != nullptr;
WindowBaseImpl::Show(activate, isDialog); WindowBaseImpl::Show(activate, isDialog);
@ -96,6 +96,8 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) {
auto cparent = dynamic_cast<WindowImpl *>(parent); auto cparent = dynamic_cast<WindowImpl *>(parent);
_parent = cparent; _parent = cparent;
_isDialog = _parent != nullptr;
if(_parent != nullptr && Window != 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 // 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 @end
extern void InitializeAvnApp(IAvnApplicationEvents* events) extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate)
{ {
NSApplication* app = [AvnApplication sharedApplication]; if(!disableAppDelegate)
id delegate = [[AvnAppDelegate alloc] initWithEvents:events]; {
[app setDelegate:delegate]; NSApplication* app = [AvnApplication sharedApplication];
id delegate = [[AvnAppDelegate alloc] initWithEvents:events];
[app setDelegate:delegate];
}
} }
HRESULT AvnApplicationCommands::HideApp() HRESULT AvnApplicationCommands::HideApp()

2
native/Avalonia.Native/src/OSX/common.h

@ -32,7 +32,7 @@ extern void SetServicesMenu (IAvnMenu* menu);
extern IAvnMenu* GetAppMenu (); extern IAvnMenu* GetAppMenu ();
extern NSMenuItem* GetAppMenuItem (); extern NSMenuItem* GetAppMenuItem ();
extern void InitializeAvnApp(IAvnApplicationEvents* events); extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate);
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
extern NSPoint ToNSPoint (AvnPoint p); extern NSPoint ToNSPoint (AvnPoint p);
extern NSRect ToNSRect (AvnRect r); extern NSRect ToNSRect (AvnRect r);

41
native/Avalonia.Native/src/OSX/main.mm

@ -3,6 +3,8 @@
#include "common.h" #include "common.h"
static NSString* s_appTitle = @"Avalonia"; static NSString* s_appTitle = @"Avalonia";
static int disableSetProcessName = 0;
static bool disableAppDelegate = false;
// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
@ -111,11 +113,17 @@ public:
@autoreleasepool @autoreleasepool
{ {
auto appTitle = [NSString stringWithUTF8String: utf8String]; auto appTitle = [NSString stringWithUTF8String: utf8String];
if (disableSetProcessName == 0)
[[NSProcessInfo processInfo] setProcessName:appTitle]; {
[[NSProcessInfo processInfo] setProcessName:appTitle];
SetProcessName(appTitle); SetProcessName(appTitle);
}
if (disableSetProcessName == 1)
{
auto rootMenu = [NSApp mainMenu];
[rootMenu setTitle:appTitle];
}
return S_OK; 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: /// See "Using POSIX Threads in a Cocoa Application" section here:
@ -175,7 +204,7 @@ public:
@autoreleasepool{ @autoreleasepool{
[[ThreadingInitializer new] do]; [[ThreadingInitializer new] do];
} }
InitializeAvnApp(events); InitializeAvnApp(events, disableAppDelegate);
return S_OK; 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]; [_layer setContents: (__bridge IOSurface*) surface->surface];
} }
[CATransaction commit]; [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 else
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{

19
nukebuild/Build.cs

@ -145,8 +145,25 @@ partial class Build : NukeBuild
{ {
Information($"Running tests from {projectName}"); Information($"Running tests from {projectName}");
var project = Solution.GetProject(projectName).NotNull("project != null"); 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") if (fw.StartsWith("net4")
&& RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && RuntimeInformation.IsOSPlatform(OSPlatform.Linux)

6
nukebuild/_build.csproj

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

3
packages/Avalonia/AvaloniaBuildTasks.targets

@ -3,7 +3,6 @@
<_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild)</_AvaloniaUseExternalMSBuild> <_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild)</_AvaloniaUseExternalMSBuild>
<_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false</_AvaloniaUseExternalMSBuild> <_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false</_AvaloniaUseExternalMSBuild>
<AvaloniaXamlReportImportance Condition="'$(AvaloniaXamlReportImportance)' == ''">low</AvaloniaXamlReportImportance> <AvaloniaXamlReportImportance Condition="'$(AvaloniaXamlReportImportance)' == ''">low</AvaloniaXamlReportImportance>
<_AvaloniaPatchComInterop Condition="'$(_AvaloniaPatchComInterop)' == ''">false</_AvaloniaPatchComInterop>
<_AvaloniaSkipXamlCompilation Condition="'$(_AvaloniaSkipXamlCompilation)' == ''">false</_AvaloniaSkipXamlCompilation> <_AvaloniaSkipXamlCompilation Condition="'$(_AvaloniaSkipXamlCompilation)' == ''">false</_AvaloniaSkipXamlCompilation>
</PropertyGroup> </PropertyGroup>
@ -71,7 +70,6 @@
Output="$(AvaloniaResourcesTemporaryFilePath)" Output="$(AvaloniaResourcesTemporaryFilePath)"
Root="$(MSBuildProjectDirectory)" Root="$(MSBuildProjectDirectory)"
Resources="@(AvaloniaResource)" Resources="@(AvaloniaResource)"
EmbeddedResources="@(EmbeddedResources)"
ReportImportance="$(AvaloniaXamlReportImportance)"/> ReportImportance="$(AvaloniaXamlReportImportance)"/>
<Exec <Exec
Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'" Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"
@ -106,7 +104,6 @@
AssemblyOriginatorKeyFile="$(AssemblyOriginatorKeyFile)" AssemblyOriginatorKeyFile="$(AssemblyOriginatorKeyFile)"
SignAssembly="$(SignAssembly)" SignAssembly="$(SignAssembly)"
DelaySign="$(DelaySign)" DelaySign="$(DelaySign)"
EnableComInteropPatching="$(_AvaloniaPatchComInterop)"
SkipXamlCompilation="$(_AvaloniaSkipXamlCompilation)" SkipXamlCompilation="$(_AvaloniaSkipXamlCompilation)"
DebuggerLaunch="$(AvaloniaXamlIlDebuggerLaunch)" DebuggerLaunch="$(AvaloniaXamlIlDebuggerLaunch)"
/> />

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 ## 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)* *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 ## .NET Foundation

1
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -5,6 +5,7 @@
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch> <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RuntimeFrameworkVersion>6.0.9</RuntimeFrameworkVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(RunNativeAotCompilation)' == 'true'"> <PropertyGroup Condition="'$(RunNativeAotCompilation)' == 'true'">

5
samples/ControlCatalog.Web/ControlCatalog.Web.csproj

@ -16,9 +16,8 @@
<TrimMode>full</TrimMode> <TrimMode>full</TrimMode>
<WasmBuildNative>true</WasmBuildNative> <WasmBuildNative>true</WasmBuildNative>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<WasmEnableSIMD>true</WasmEnableSIMD> <EmccCompileOptimizationFlag>-O2</EmccCompileOptimizationFlag>
<EmccCompileOptimizationFlag>-O3</EmccCompileOptimizationFlag> <EmccLinkOptimizationFlag>-O2</EmccLinkOptimizationFlag>
<EmccLinkOptimizationFlag>-O3</EmccLinkOptimizationFlag>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

1
samples/ControlCatalog.Web/Roots.xml

@ -3,4 +3,5 @@
<assembly fullname="ControlCatalog.Web" preserve="All" /> <assembly fullname="ControlCatalog.Web" preserve="All" />
<assembly fullname="Avalonia.Themes.Fluent" preserve="All" /> <assembly fullname="Avalonia.Themes.Fluent" preserve="All" />
<assembly fullname="Avalonia.Themes.Simple" preserve="All" /> <assembly fullname="Avalonia.Themes.Simple" preserve="All" />
<assembly fullname="Avalonia.Controls.ColorPicker" preserve="All" />
</linker> </linker>

21
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -24,18 +24,19 @@
HsvColor="hsv(120, 1, 1)" HsvColor="hsv(120, 1, 1)"
Margin="0,50,0,0"> Margin="0,50,0,0">
<ColorPicker.Palette> <ColorPicker.Palette>
<controls:FlatColorPalette /> <controls:FlatHalfColorPalette />
</ColorPicker.Palette> </ColorPicker.Palette>
</ColorPicker> </ColorPicker>
<Grid Grid.Column="2" <Grid Grid.Column="2"
Grid.Row="0" Grid.Row="0"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<ColorSpectrum x:Name="ColorSpectrum1" <ColorSpectrum x:Name="ColorSpectrum1"
Grid.Row="0" Grid.Row="0"
Color="Red" Color="Red"
CornerRadius="10" CornerRadius="10"
Height="256" Height="256"
Width="256" /> Width="256" />
<!-- HSV Sliders -->
<ColorSlider Grid.Row="1" <ColorSlider Grid.Row="1"
Margin="0,10,0,0" Margin="0,10,0,0"
ColorComponent="Component1" ColorComponent="Component1"
@ -53,7 +54,21 @@
ColorComponent="Alpha" ColorComponent="Alpha"
ColorModel="Hsva" ColorModel="Hsva"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" /> HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
<ColorPreviewer Grid.Row="5" <!-- RGB Sliders -->
<!--<ColorSlider Grid.Row="5"
Margin="0,10,0,0"
ColorComponent="Component1"
ColorModel="Rgba"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
<ColorSlider Grid.Row="6"
ColorComponent="Component2"
ColorModel="Rgba"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
<ColorSlider Grid.Row="7"
ColorComponent="Component3"
ColorModel="Rgba"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />-->
<ColorPreviewer Grid.Row="8"
IsAccentColorsVisible="False" IsAccentColorsVisible="False"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" /> HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
</Grid> </Grid>

1
samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs

@ -20,6 +20,7 @@ namespace ControlCatalog.Pages
Color = Colors.Blue, Color = Colors.Blue,
Margin = new Thickness(0, 50, 0, 0), Margin = new Thickness(0, 50, 0, 0),
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
Palette = new MaterialHalfColorPalette(),
}; };
Grid.SetColumn(colorPicker, 2); Grid.SetColumn(colorPicker, 2);
Grid.SetRow(colorPicker, 1); Grid.SetRow(colorPicker, 1);

17
samples/ControlCatalog/Pages/ExpanderPage.xaml

@ -32,6 +32,23 @@
<TextBlock>Expanded content</TextBlock> <TextBlock>Expanded content</TextBlock>
</StackPanel> </StackPanel>
</Expander> </Expander>
<Expander ExpandDirection="Down"
CornerRadius="{Binding CornerRadius}">
<Expander.Header>
<Button Content="Control in Header" />
</Expander.Header>
<StackPanel>
<TextBlock>Expanded content</TextBlock>
</StackPanel>
</Expander>
<Expander Header="Disabled"
IsEnabled="False"
ExpandDirection="Down"
CornerRadius="{Binding CornerRadius}">
<StackPanel>
<TextBlock>Expanded content</TextBlock>
</StackPanel>
</Expander>
<CheckBox IsChecked="{Binding Rounded}">Rounded</CheckBox> <CheckBox IsChecked="{Binding Rounded}">Rounded</CheckBox>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

4
samples/ControlCatalog/Pages/ScreenPage.cs

@ -62,10 +62,10 @@ namespace ControlCatalog.Pages
CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}"); CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20)); context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20));
formattedText = CreateFormattedText($"Scaling: {screen.PixelDensity * 100}%"); formattedText = CreateFormattedText($"Scaling: {screen.Scaling * 100}%");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40)); context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40));
formattedText = CreateFormattedText($"Primary: {screen.Primary}"); formattedText = CreateFormattedText($"IsPrimary: {screen.IsPrimary}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60)); context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60));
formattedText = formattedText =

4
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -118,7 +118,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<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 This <Span FontWeight="Bold">is</Span> a
<Span Background="Silver" Foreground="Maroon">TextBlock</Span> <Span Background="Silver" Foreground="Maroon">TextBlock</Span>
with <Span TextDecorations="Underline">several</Span> with <Span TextDecorations="Underline">several</Span>
@ -126,7 +126,7 @@
<Span Foreground="Blue"> <Span Foreground="Blue">
using a <Bold>variety</Bold> of <Italic>styles</Italic> using a <Bold>variety</Bold> of <Italic>styles</Italic>
</Span>. </Span>.
</RichTextBlock> </SelectableTextBlock>
</Border> </Border>
</WrapPanel> </WrapPanel>
</StackPanel> </StackPanel>

2
src/Avalonia.Base/Avalonia.Base.csproj

@ -23,6 +23,7 @@
<Import Project="..\..\build\SourceGenerators.props" /> <Import Project="..\..\build\SourceGenerators.props" />
<ItemGroup> <ItemGroup>
<Compile Include="..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" /> <Compile Include="..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
<Compile Include="..\Shared\StringCompatibilityExtensions.cs" Link="Compatibility\StringCompatibilityExtensions.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="InternalsVisibleTo"> <ItemGroup Label="InternalsVisibleTo">
@ -46,6 +47,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Compatibility\" />
<Folder Include="Rendering\Composition\Utils" /> <Folder Include="Rendering\Composition\Utils" />
</ItemGroup> </ItemGroup>
</Project> </Project>

6
src/Avalonia.Base/Collections/AvaloniaDictionary.cs

@ -81,7 +81,7 @@ namespace Avalonia.Collections
if (replace) if (replace)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]")); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));
if (CollectionChanged != null) if (CollectionChanged != null)
{ {
@ -148,7 +148,7 @@ namespace Avalonia.Collections
{ {
_inner.Remove(key); _inner.Remove(key);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); 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) if (CollectionChanged != null)
{ {
@ -208,7 +208,7 @@ namespace Avalonia.Collections
private void NotifyAdd(TKey key, TValue value) private void NotifyAdd(TKey key, TValue value)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); 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) 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/DragDrop.cs

@ -13,7 +13,7 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Event which is raised, when a drag-and-drop operation leaves the element. /// Event which is raised, when a drag-and-drop operation leaves the element.
/// </summary> /// </summary>
public static readonly RoutedEvent<RoutedEventArgs> DragLeaveEvent = RoutedEvent.Register<RoutedEventArgs>("DragLeave", RoutingStrategies.Bubble, typeof(DragDrop)); public static readonly RoutedEvent<DragEventArgs> DragLeaveEvent = RoutedEvent.Register<DragEventArgs>("DragLeave", RoutingStrategies.Bubble, typeof(DragDrop));
/// <summary> /// <summary>
/// Event which is raised, when a drag-and-drop operation is updated while over the element. /// Event which is raised, when a drag-and-drop operation is updated while over the element.
/// </summary> /// </summary>

8
src/Avalonia.Base/Input/DragDropDevice.cs

@ -54,7 +54,7 @@ namespace Avalonia.Input
try try
{ {
if (_lastTarget != null) if (_lastTarget != null)
_lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent)); RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragLeaveEvent, effects, data, modifiers);
return RaiseDragEvent(target, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers); return RaiseDragEvent(target, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers);
} }
finally finally
@ -63,13 +63,13 @@ namespace Avalonia.Input
} }
} }
private void DragLeave(IInputElement inputRoot) private void DragLeave(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers)
{ {
if (_lastTarget == null) if (_lastTarget == null)
return; return;
try try
{ {
_lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent)); RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragLeaveEvent, effects, data, modifiers);
} }
finally finally
{ {
@ -106,7 +106,7 @@ namespace Avalonia.Input
e.Effects = DragOver(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); e.Effects = DragOver(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers);
break; break;
case RawDragEventType.DragLeave: case RawDragEventType.DragLeave:
DragLeave(e.Root); DragLeave(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers);
break; break;
case RawDragEventType.Drop: case RawDragEventType.Drop:
e.Effects = Drop(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); e.Effects = Drop(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers);

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

@ -32,7 +32,7 @@ namespace Avalonia.Input
return point; 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) : base(routedEvent)
{ {
Data = data; Data = data;

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

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

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

@ -5,6 +5,11 @@ namespace Avalonia.Input
{ {
public class KeyEventArgs : RoutedEventArgs public class KeyEventArgs : RoutedEventArgs
{ {
internal KeyEventArgs()
{
}
public IKeyboardDevice? Device { get; set; } public IKeyboardDevice? Device { get; set; }
public Key Key { get; set; } public Key Key { get; set; }

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

@ -7,7 +7,7 @@ namespace Avalonia.Input
{ {
public Vector Delta { get; set; } 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, IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta) PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(routedEvent, source, pointer, rootVisual, rootVisualPosition, : 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 PointerPointProperties _properties;
private readonly Lazy<IReadOnlyList<RawPointerPoint>?>? _previousPoints; private readonly Lazy<IReadOnlyList<RawPointerPoint>?>? _previousPoints;
public PointerEventArgs(RoutedEvent routedEvent, internal PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source, IInteractive? source,
IPointer pointer, IPointer pointer,
IVisual? rootVisual, Point rootVisualPosition, IVisual? rootVisual, Point rootVisualPosition,
@ -30,8 +30,8 @@ namespace Avalonia.Input
Timestamp = timestamp; Timestamp = timestamp;
KeyModifiers = modifiers; KeyModifiers = modifiers;
} }
public PointerEventArgs(RoutedEvent routedEvent, internal PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source, IInteractive? source,
IPointer pointer, IPointer pointer,
IVisual? rootVisual, Point rootVisualPosition, IVisual? rootVisual, Point rootVisualPosition,
@ -124,7 +124,7 @@ namespace Avalonia.Input
public class PointerPressedEventArgs : PointerEventArgs public class PointerPressedEventArgs : PointerEventArgs
{ {
public PointerPressedEventArgs( internal PointerPressedEventArgs(
IInteractive source, IInteractive source,
IPointer pointer, IPointer pointer,
IVisual rootVisual, Point rootVisualPosition, IVisual rootVisual, Point rootVisualPosition,
@ -143,7 +143,7 @@ namespace Avalonia.Input
public class PointerReleasedEventArgs : PointerEventArgs public class PointerReleasedEventArgs : PointerEventArgs
{ {
public PointerReleasedEventArgs( internal PointerReleasedEventArgs(
IInteractive source, IPointer pointer, IInteractive source, IPointer pointer,
IVisual rootVisual, Point rootVisualPosition, ulong timestamp, IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, PointerPointProperties properties, KeyModifiers modifiers,
@ -164,7 +164,7 @@ namespace Avalonia.Input
{ {
public IPointer Pointer { get; } public IPointer Pointer { get; }
public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) internal PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
{ {
Pointer = pointer; Pointer = pointer;
Source = source; Source = source;

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

@ -7,7 +7,7 @@ namespace Avalonia.Input
{ {
public Vector Delta { get; set; } 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, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta) PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, : 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; private static int _nextId = 1;
public static int GetNextFreeId() => _nextId++; 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; Id = id;
Delta = delta; Delta = delta;
@ -21,7 +21,7 @@ namespace Avalonia.Input
{ {
public int Id { get; } public int Id { get; }
public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) internal ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
{ {
Id = id; Id = id;
} }

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

@ -7,7 +7,7 @@ namespace Avalonia.Input
{ {
private readonly PointerEventArgs lastPointerEventArgs; private readonly PointerEventArgs lastPointerEventArgs;
public TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs) internal TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs)
: base(routedEvent) : base(routedEvent)
{ {
this.lastPointerEventArgs = lastPointerEventArgs; this.lastPointerEventArgs = lastPointerEventArgs;

4
src/Avalonia.Base/Input/TextInputEventArgs.cs

@ -4,6 +4,10 @@ namespace Avalonia.Input
{ {
public class TextInputEventArgs : RoutedEventArgs public class TextInputEventArgs : RoutedEventArgs
{ {
internal TextInputEventArgs()
{
}
public IKeyboardDevice? Device { get; set; } public IKeyboardDevice? Device { get; set; }
public string? Text { get; set; } public string? Text { get; set; }

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

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

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

@ -2,29 +2,59 @@ using System;
namespace Avalonia.Interactivity namespace Avalonia.Interactivity
{ {
/// <summary>
/// Provides state information and data specific to a routed event.
/// </summary>
public class RoutedEventArgs : EventArgs public class RoutedEventArgs : EventArgs
{ {
/// <summary>
/// Initializes a new instance of the <see cref="RoutedEventArgs"/> class.
/// </summary>
public RoutedEventArgs() 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) public RoutedEventArgs(RoutedEvent? routedEvent)
{ {
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) public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
{ {
RoutedEvent = routedEvent; RoutedEvent = routedEvent;
Source = source; 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; } public bool Handled { get; set; }
/// <summary>
/// Gets or sets the routed event associated with these event args.
/// </summary>
public RoutedEvent? RoutedEvent { get; set; } 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; } public RoutingStrategies Route { get; set; }
/// <summary>
/// Gets or sets the source object that raised the routed event.
/// </summary>
public IInteractive? Source { get; set; } public IInteractive? Source { get; set; }
} }
} }

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

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

6
src/Avalonia.Base/Layout/UniformGridLayoutState.cs

@ -117,12 +117,12 @@ namespace Avalonia.Layout
double extraMinorPixelsForEachItem = 0.0; double extraMinorPixelsForEachItem = 0.0;
if (!double.IsInfinity(availableSizeMinor)) if (!double.IsInfinity(availableSizeMinor))
{ {
var numItemsPerColumn = Math.Min( var numItemsPerColumn = (int)Math.Min(
maxItemsPerLine, maxItemsPerLine,
Math.Max(1.0, availableSizeMinor / (itemSizeMinor + minorItemSpacing))); Math.Max(1.0, availableSizeMinor / (itemSizeMinor + minorItemSpacing)));
var usedSpace = (numItemsPerColumn * (itemSizeMinor + minorItemSpacing)) - minorItemSpacing; var usedSpace = (numItemsPerColumn * (itemSizeMinor + minorItemSpacing)) - minorItemSpacing;
var remainingSpace = ((int)(availableSizeMinor - usedSpace)); var remainingSpace = availableSizeMinor - usedSpace;
extraMinorPixelsForEachItem = remainingSpace / ((int)numItemsPerColumn); extraMinorPixelsForEachItem = (int)(remainingSpace / numItemsPerColumn);
} }
if (stretch == UniformGridLayoutItemsStretch.Fill) if (stretch == UniformGridLayoutItemsStretch.Fill)

49
src/Avalonia.Base/Media/Color.cs

@ -9,6 +9,7 @@ using System;
using System.Globalization; using System.Globalization;
#if !BUILDTASK #if !BUILDTASK
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
using static Avalonia.Utilities.SpanHelpers;
#endif #endif
namespace Avalonia.Media namespace Avalonia.Media
@ -295,9 +296,7 @@ namespace Avalonia.Media
return false; return false;
} }
// TODO: (netstandard 2.1) Can use allocation free parsing. if (!input.TryParseUInt(NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
if (!uint.TryParse(input.ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture,
out var parsed))
{ {
return false; return false;
} }
@ -382,9 +381,9 @@ namespace Avalonia.Media
if (components.Length == 3) // RGB if (components.Length == 3) // RGB
{ {
if (InternalTryParseByte(components[0], out byte red) && if (InternalTryParseByte(components[0].AsSpan(), out byte red) &&
InternalTryParseByte(components[1], out byte green) && InternalTryParseByte(components[1].AsSpan(), out byte green) &&
InternalTryParseByte(components[2], out byte blue)) InternalTryParseByte(components[2].AsSpan(), out byte blue))
{ {
color = new Color(0xFF, red, green, blue); color = new Color(0xFF, red, green, blue);
return true; return true;
@ -392,10 +391,10 @@ namespace Avalonia.Media
} }
else if (components.Length == 4) // RGBA else if (components.Length == 4) // RGBA
{ {
if (InternalTryParseByte(components[0], out byte red) && if (InternalTryParseByte(components[0].AsSpan(), out byte red) &&
InternalTryParseByte(components[1], out byte green) && InternalTryParseByte(components[1].AsSpan(), out byte green) &&
InternalTryParseByte(components[2], out byte blue) && InternalTryParseByte(components[2].AsSpan(), out byte blue) &&
InternalTryParseDouble(components[3], out double alpha)) InternalTryParseDouble(components[3].AsSpan(), out double alpha))
{ {
color = new Color((byte)Math.Round(alpha * 255.0), red, green, blue); color = new Color((byte)Math.Round(alpha * 255.0), red, green, blue);
return true; return true;
@ -403,17 +402,14 @@ namespace Avalonia.Media
} }
// Local function to specially parse a byte value with an optional percentage sign // Local function to specially parse a byte value with an optional percentage sign
bool InternalTryParseByte(string inString, out byte outByte) bool InternalTryParseByte(ReadOnlySpan<char> inString, out byte outByte)
{ {
// The percent sign, if it exists, must be at the end of the number // The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); int percentIndex = inString.IndexOf("%".AsSpan(), StringComparison.Ordinal);
if (percentIndex >= 0) if (percentIndex >= 0)
{ {
var result = double.TryParse( var result = inString.Slice(0, percentIndex).TryParseDouble(NumberStyles.Number, CultureInfo.InvariantCulture,
inString.Substring(0, percentIndex),
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double percentage); out double percentage);
outByte = (byte)Math.Round((percentage / 100.0) * 255.0); outByte = (byte)Math.Round((percentage / 100.0) * 255.0);
@ -421,37 +417,28 @@ namespace Avalonia.Media
} }
else else
{ {
return byte.TryParse( return inString.TryParseByte(NumberStyles.Number, CultureInfo.InvariantCulture,
inString,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out outByte); out outByte);
} }
} }
// Local function to specially parse a double value with an optional percentage sign // Local function to specially parse a double value with an optional percentage sign
bool InternalTryParseDouble(string inString, out double outDouble) bool InternalTryParseDouble(ReadOnlySpan<char> inString, out double outDouble)
{ {
// The percent sign, if it exists, must be at the end of the number // The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); int percentIndex = inString.IndexOf("%".AsSpan(), StringComparison.Ordinal);
if (percentIndex >= 0) if (percentIndex >= 0)
{ {
var result = double.TryParse( var result = inString.Slice(0, percentIndex).TryParseDouble(NumberStyles.Number, CultureInfo.InvariantCulture,
inString.Substring(0, percentIndex), out double percentage);
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double percentage);
outDouble = percentage / 100.0; outDouble = percentage / 100.0;
return result; return result;
} }
else else
{ {
return double.TryParse( return inString.TryParseDouble(NumberStyles.Number, CultureInfo.InvariantCulture,
inString,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out outDouble); out outDouble);
} }
} }

27
src/Avalonia.Base/Media/FontSimulations.cs

@ -0,0 +1,27 @@
using System;
namespace Avalonia.Media
{
/// <summary>
/// Specifies algorithmic style simulations to be applied to the typeface.
/// Bold and oblique simulations can be combined via bitwise OR operation.
/// </summary>
[Flags]
public enum FontSimulations : byte
{
/// <summary>
/// No simulations are performed.
/// </summary>
None = 0x0000,
/// <summary>
/// Algorithmic emboldening is performed.
/// </summary>
Bold = 0x0001,
/// <summary>
/// Algorithmic italicization is performed.
/// </summary>
Oblique = 0x0002
}
}

3
src/Avalonia.Base/Media/FormattedText.cs

@ -93,7 +93,8 @@ namespace Avalonia.Media
runProps, runProps,
TextWrapping.WrapWithOverflow, TextWrapping.WrapWithOverflow,
0, // line height not specified 0, // line height not specified
0 // indentation not specified 0, // indentation not specified
0
); );
InvalidateMetrics(); InvalidateMetrics();

24
src/Avalonia.Base/Media/GlyphMetrics.cs

@ -0,0 +1,24 @@
namespace Avalonia.Media;
public readonly struct GlyphMetrics
{
/// <summary>
/// Distance from the x-origin to the left extremum of the glyph.
/// </summary>
public int XBearing { get; init; }
/// <summary>
/// Distance from the top extremum of the glyph to the y-origin.
/// </summary>
public int YBearing{ get; init; }
/// <summary>
/// Distance from the left extremum of the glyph to the right extremum.
/// </summary>
public int Width{ get; init; }
/// <summary>
/// Distance from the top extremum of the glyph to the bottom extremum.
/// </summary>
public int Height{ get; init; }
}

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

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

30
src/Avalonia.Base/Media/HslColor.cs

@ -302,9 +302,9 @@ namespace Avalonia.Media
if (components.Length == 3) // HSL if (components.Length == 3) // HSL
{ {
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && if (components[0].AsSpan().TryParseDouble(NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
TryInternalParse(components[1], out double saturation) && TryInternalParse(components[1].AsSpan(), out double saturation) &&
TryInternalParse(components[2], out double lightness)) TryInternalParse(components[2].AsSpan(), out double lightness))
{ {
hslColor = new HslColor(1.0, hue, saturation, lightness); hslColor = new HslColor(1.0, hue, saturation, lightness);
return true; return true;
@ -312,10 +312,10 @@ namespace Avalonia.Media
} }
else if (components.Length == 4) // HSLA else if (components.Length == 4) // HSLA
{ {
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && if (components[0].AsSpan().TryParseDouble(NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
TryInternalParse(components[1], out double saturation) && TryInternalParse(components[1].AsSpan(), out double saturation) &&
TryInternalParse(components[2], out double lightness) && TryInternalParse(components[2].AsSpan(), out double lightness) &&
TryInternalParse(components[3], out double alpha)) TryInternalParse(components[3].AsSpan(), out double alpha))
{ {
hslColor = new HslColor(alpha, hue, saturation, lightness); hslColor = new HslColor(alpha, hue, saturation, lightness);
return true; return true;
@ -323,28 +323,22 @@ namespace Avalonia.Media
} }
// Local function to specially parse a double value with an optional percentage sign // Local function to specially parse a double value with an optional percentage sign
bool TryInternalParse(string inString, out double outDouble) bool TryInternalParse(ReadOnlySpan<char> inString, out double outDouble)
{ {
// The percent sign, if it exists, must be at the end of the number // The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); int percentIndex = inString.IndexOf("%".AsSpan(), StringComparison.Ordinal);
if (percentIndex >= 0) if (percentIndex >= 0)
{ {
var result = double.TryParse( var result = inString.Slice(0, percentIndex).TryParseDouble(NumberStyles.Number, CultureInfo.InvariantCulture,
inString.Substring(0, percentIndex), out double percentage);
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double percentage);
outDouble = percentage / 100.0; outDouble = percentage / 100.0;
return result; return result;
} }
else else
{ {
return double.TryParse( return inString.TryParseDouble(NumberStyles.Number, CultureInfo.InvariantCulture,
inString,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out outDouble); out outDouble);
} }
} }

30
src/Avalonia.Base/Media/HsvColor.cs

@ -302,9 +302,9 @@ namespace Avalonia.Media
if (components.Length == 3) // HSV if (components.Length == 3) // HSV
{ {
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && if (components[0].AsSpan().TryParseDouble(NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
TryInternalParse(components[1], out double saturation) && TryInternalParse(components[1].AsSpan(), out double saturation) &&
TryInternalParse(components[2], out double value)) TryInternalParse(components[2].AsSpan(), out double value))
{ {
hsvColor = new HsvColor(1.0, hue, saturation, value); hsvColor = new HsvColor(1.0, hue, saturation, value);
return true; return true;
@ -312,10 +312,10 @@ namespace Avalonia.Media
} }
else if (components.Length == 4) // HSVA else if (components.Length == 4) // HSVA
{ {
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && if (components[0].AsSpan().TryParseDouble(NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
TryInternalParse(components[1], out double saturation) && TryInternalParse(components[1].AsSpan(), out double saturation) &&
TryInternalParse(components[2], out double value) && TryInternalParse(components[2].AsSpan(), out double value) &&
TryInternalParse(components[3], out double alpha)) TryInternalParse(components[3].AsSpan(), out double alpha))
{ {
hsvColor = new HsvColor(alpha, hue, saturation, value); hsvColor = new HsvColor(alpha, hue, saturation, value);
return true; return true;
@ -323,28 +323,22 @@ namespace Avalonia.Media
} }
// Local function to specially parse a double value with an optional percentage sign // Local function to specially parse a double value with an optional percentage sign
bool TryInternalParse(string inString, out double outDouble) bool TryInternalParse(ReadOnlySpan<char> inString, out double outDouble)
{ {
// The percent sign, if it exists, must be at the end of the number // The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); int percentIndex = inString.IndexOf("%".AsSpan(), StringComparison.Ordinal);
if (percentIndex >= 0) if (percentIndex >= 0)
{ {
var result = double.TryParse( var result = inString.Slice(0, percentIndex).TryParseDouble(NumberStyles.Number, CultureInfo.InvariantCulture,
inString.Substring(0, percentIndex), out double percentage);
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double percentage);
outDouble = percentage / 100.0; outDouble = percentage / 100.0;
return result; return result;
} }
else else
{ {
return double.TryParse( return inString.TryParseDouble(NumberStyles.Number, CultureInfo.InvariantCulture,
inString,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out outDouble); out outDouble);
} }
} }

15
src/Avalonia.Base/Media/IGlyphTypeface.cs

@ -19,6 +19,21 @@ namespace Avalonia.Media
/// </returns> /// </returns>
FontMetrics Metrics { get; } FontMetrics Metrics { get; }
/// <summary>
/// Gets the algorithmic style simulations applied to this glyph typeface.
/// </summary>
FontSimulations FontSimulations { get; }
/// <summary>
/// Tries to get a glyph's metrics in em units.
/// </summary>
/// <param name="glyph">The glyph id.</param>
/// <param name="metrics">The glyph metrics.</param>
/// <returns>
/// <c>true</c> if an glyph's metrics was found, <c>false</c> otherwise.
/// </returns>
bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics);
/// <summary> /// <summary>
/// Returns an glyph index for the specified codepoint. /// Returns an glyph index for the specified codepoint.
/// </summary> /// </summary>

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

@ -121,18 +121,28 @@ namespace Avalonia.Media.Imaging
/// Saves the bitmap to a file. /// Saves the bitmap to a file.
/// </summary> /// </summary>
/// <param name="fileName">The filename.</param> /// <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> /// <summary>
/// Saves the bitmap to a stream. /// Saves the bitmap to a stream.
/// </summary> /// </summary>
/// <param name="stream">The stream.</param> /// <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/> /// <inheritdoc/>

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

@ -35,12 +35,22 @@ namespace Avalonia.Media.Imaging
/// Saves the bitmap to a file. /// Saves the bitmap to a file.
/// </summary> /// </summary>
/// <param name="fileName">The filename.</param> /// <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> /// <summary>
/// Saves the bitmap to a stream in png format. /// Saves the bitmap to a stream in png format.
/// </summary> /// </summary>
/// <param name="stream">The stream.</param> /// <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; _isOpen = true;
} }
private void SetFillRule(ref ReadOnlySpan<char> span) private void SetFillRule(
#if NET7SDK
scoped
#endif
ref ReadOnlySpan<char> span)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
@ -452,7 +456,11 @@ namespace Avalonia.Media
return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || span[0] == '.' || char.IsDigit(span[0])); return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || span[0] == '.' || char.IsDigit(span[0]));
} }
private static bool ReadArgument(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); remaining = SkipWhitespace(remaining);
if (remaining.IsEmpty) if (remaining.IsEmpty)

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

@ -17,15 +17,18 @@
/// <param name="textAlignment">logical horizontal alignment</param> /// <param name="textAlignment">logical horizontal alignment</param>
/// <param name="textWrap">text wrap option</param> /// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param> /// <param name="lineHeight">Paragraph line height</param>
/// <param name="letterSpacing">letter spacing</param>
public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties, public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties,
TextAlignment textAlignment = TextAlignment.Left, TextAlignment textAlignment = TextAlignment.Left,
TextWrapping textWrap = TextWrapping.NoWrap, TextWrapping textWrap = TextWrapping.NoWrap,
double lineHeight = 0) double lineHeight = 0,
double letterSpacing = 0)
{ {
DefaultTextRunProperties = defaultTextRunProperties; DefaultTextRunProperties = defaultTextRunProperties;
_textAlignment = textAlignment; _textAlignment = textAlignment;
_textWrap = textWrap; _textWrap = textWrap;
_lineHeight = lineHeight; _lineHeight = lineHeight;
LetterSpacing = letterSpacing;
} }
/// <summary> /// <summary>
@ -39,6 +42,7 @@
/// <param name="textWrap">text wrap option</param> /// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param> /// <param name="lineHeight">Paragraph line height</param>
/// <param name="indent">line indentation</param> /// <param name="indent">line indentation</param>
/// <param name="letterSpacing">letter spacing</param>
public GenericTextParagraphProperties( public GenericTextParagraphProperties(
FlowDirection flowDirection, FlowDirection flowDirection,
TextAlignment textAlignment, TextAlignment textAlignment,
@ -47,8 +51,8 @@
TextRunProperties defaultTextRunProperties, TextRunProperties defaultTextRunProperties,
TextWrapping textWrap, TextWrapping textWrap,
double lineHeight, double lineHeight,
double indent double indent,
) double letterSpacing)
{ {
_flowDirection = flowDirection; _flowDirection = flowDirection;
_textAlignment = textAlignment; _textAlignment = textAlignment;
@ -57,6 +61,7 @@
DefaultTextRunProperties = defaultTextRunProperties; DefaultTextRunProperties = defaultTextRunProperties;
_textWrap = textWrap; _textWrap = textWrap;
_lineHeight = lineHeight; _lineHeight = lineHeight;
LetterSpacing = letterSpacing;
Indent = indent; Indent = indent;
} }
@ -72,7 +77,8 @@
textParagraphProperties.DefaultTextRunProperties, textParagraphProperties.DefaultTextRunProperties,
textParagraphProperties.TextWrapping, textParagraphProperties.TextWrapping,
textParagraphProperties.LineHeight, textParagraphProperties.LineHeight,
textParagraphProperties.Indent) textParagraphProperties.Indent,
textParagraphProperties.LetterSpacing)
{ {
} }
@ -131,6 +137,11 @@
/// </summary> /// </summary>
public override double Indent { get; } public override double Indent { get; }
/// <summary>
/// The letter spacing
/// </summary>
public override double LetterSpacing { get; }
/// <summary> /// <summary>
/// Set text flow direction /// Set text flow direction
/// </summary> /// </summary>

36
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -249,7 +249,8 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
currentRun.Properties.FontRenderingEmSize, 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)); drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
@ -477,32 +478,35 @@ namespace Avalonia.Media.TextFormatting
{ {
case ShapedTextCharacters shapedTextCharacters: case ShapedTextCharacters shapedTextCharacters:
{ {
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0]; if(shapedTextCharacters.ShapedBuffer.Length > 0)
var lastCluster = firstCluster;
for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
{ {
var glyphInfo = shapedTextCharacters.ShapedBuffer[i]; var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
var lastCluster = firstCluster;
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
{ {
measuredLength += Math.Max(0, lastCluster - firstCluster); var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
goto found; if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
} {
measuredLength += Math.Max(0, lastCluster - firstCluster);
lastCluster = glyphInfo.GlyphCluster; goto found;
currentWidth += glyphInfo.GlyphAdvance; }
}
measuredLength += currentRun.TextSourceLength; lastCluster = glyphInfo.GlyphCluster;
currentWidth += glyphInfo.GlyphAdvance;
}
measuredLength += currentRun.TextSourceLength;
}
break; break;
} }
case { } drawableTextRun: case { } drawableTextRun:
{ {
if (currentWidth + drawableTextRun.Size.Width > paragraphWidth) if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{ {
goto found; goto found;
} }
@ -665,7 +669,7 @@ namespace Avalonia.Media.TextFormatting
if (!breakFound) if (!breakFound)
{ {
currentLength += currentRun.Text.Length; currentLength += currentRun.TextSourceLength;
continue; 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="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param> /// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</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="maxLines">The maximum number of text lines.</param>
/// <param name="textStyleOverrides">The text style overrides.</param> /// <param name="textStyleOverrides">The text style overrides.</param>
public TextLayout( public TextLayout(
@ -46,12 +47,13 @@ namespace Avalonia.Media.TextFormatting
double maxWidth = double.PositiveInfinity, double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity, double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN, double lineHeight = double.NaN,
double letterSpacing = 0,
int maxLines = 0, int maxLines = 0,
IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null) IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null)
{ {
_paragraphProperties = _paragraphProperties =
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, flowDirection, lineHeight); textDecorations, flowDirection, lineHeight, letterSpacing);
_textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides); _textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
@ -63,6 +65,8 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight; MaxHeight = maxHeight;
LetterSpacing = letterSpacing;
MaxLines = maxLines; MaxLines = maxLines;
TextLines = CreateTextLines(); TextLines = CreateTextLines();
@ -77,6 +81,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="maxWidth">The maximum width.</param> /// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param> /// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</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="maxLines">The maximum number of text lines.</param>
public TextLayout( public TextLayout(
ITextSource textSource, ITextSource textSource,
@ -85,6 +90,7 @@ namespace Avalonia.Media.TextFormatting
double maxWidth = double.PositiveInfinity, double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity, double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN, double lineHeight = double.NaN,
double letterSpacing = 0,
int maxLines = 0) int maxLines = 0)
{ {
_textSource = textSource; _textSource = textSource;
@ -99,6 +105,8 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight; MaxHeight = maxHeight;
LetterSpacing = letterSpacing;
MaxLines = maxLines; MaxLines = maxLines;
TextLines = CreateTextLines(); TextLines = CreateTextLines();
@ -128,6 +136,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
public int MaxLines { get; } public int MaxLines { get; }
/// <summary>
/// Gets the text spacing.
/// </summary>
public double LetterSpacing { get; }
/// <summary> /// <summary>
/// Gets the text lines. /// Gets the text lines.
/// </summary> /// </summary>
@ -374,15 +387,17 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textDecorations">The text decorations.</param> /// <param name="textDecorations">The text decorations.</param>
/// <param name="flowDirection">The text flow direction.</param> /// <param name="flowDirection">The text flow direction.</param>
/// <param name="lineHeight">The height of each line of text.</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> /// <returns></returns>
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping, 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); var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
return new GenericTextParagraphProperties(flowDirection, textAlignment, true, false, return new GenericTextParagraphProperties(flowDirection, textAlignment, true, false,
textRunStyle, textWrapping, lineHeight, 0); textRunStyle, textWrapping, lineHeight, 0, letterSpacing);
} }
/// <summary> /// <summary>

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

@ -57,7 +57,7 @@
public abstract double Indent { get; } public abstract double Indent { get; }
/// <summary> /// <summary>
/// Paragraph indentation /// Get the paragraph indentation.
/// </summary> /// </summary>
public virtual double ParagraphIndent public virtual double ParagraphIndent
{ {
@ -65,11 +65,16 @@
} }
/// <summary> /// <summary>
/// Default Incremental Tab /// Gets the default incremental tab width.
/// </summary> /// </summary>
public virtual double DefaultIncrementalTab public virtual double DefaultIncrementalTab
{ {
get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; } 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, double fontRenderingEmSize = 12,
sbyte bidiLevel = 0, sbyte bidiLevel = 0,
CultureInfo? culture = null, CultureInfo? culture = null,
double incrementalTabWidth = 0) double incrementalTabWidth = 0,
double letterSpacing = 0)
{ {
Typeface = typeface; Typeface = typeface;
FontRenderingEmSize = fontRenderingEmSize; FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel; BidiLevel = bidiLevel;
Culture = culture; Culture = culture;
IncrementalTabWidth = incrementalTabWidth; IncrementalTabWidth = incrementalTabWidth;
LetterSpacing = letterSpacing;
} }
/// <summary> /// <summary>
@ -45,5 +47,10 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
public double IncrementalTabWidth { get; } public double IncrementalTabWidth { get; }
/// <summary>
/// Get the letter spacing.
/// </summary>
public double LetterSpacing { get; }
} }
} }

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

@ -29,12 +29,22 @@ namespace Avalonia.Platform
/// Saves the bitmap to a file. /// Saves the bitmap to a file.
/// </summary> /// </summary>
/// <param name="fileName">The filename.</param> /// <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> /// <summary>
/// Saves the bitmap to a stream in png format. /// Saves the bitmap to a stream in png format.
/// </summary> /// </summary>
/// <param name="stream">The stream.</param> /// <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); IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride);
/// <summary> /// <summary>
/// Allocates a platform glyph run buffer. /// Creates a platform implementation of a glyph run.
/// </summary> /// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param> /// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The font rendering em size.</param> /// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="length">The length.</param> /// <param name="glyphIndices">The glyph indices.</param>
/// <returns>An <see cref="IGlyphRunBuffer"/>.</returns> /// <param name="glyphAdvances">The glyph advances.</param>
/// <remarks> /// <param name="glyphOffsets">The glyph offsets.</param>
/// This buffer only holds glyph indices. /// <returns></returns>
/// </remarks> IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double>? glyphAdvances, IReadOnlyList<Vector>? glyphOffsets);
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);
/// <summary> /// <summary>
/// Gets a value indicating whether the platform directly supports rectangles with rounded corners. /// Gets a value indicating whether the platform directly supports rectangles with rounded corners.

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

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

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

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

49
src/Avalonia.Base/Utilities/SpanHelpers.cs

@ -0,0 +1,49 @@
using System;
using System.Globalization;
using System.Runtime.CompilerServices;
namespace Avalonia.Utilities
{
public static class SpanHelpers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryParseUInt(this ReadOnlySpan<char> span, NumberStyles style, IFormatProvider provider, out uint value)
{
#if NETSTANDARD2_0
return uint.TryParse(span.ToString(), style, provider, out value);
#else
return uint.TryParse(span, style, provider, out value);
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryParseInt(this ReadOnlySpan<char> span, out int value)
{
#if NETSTANDARD2_0
return int.TryParse(span.ToString(), out value);
#else
return int.TryParse(span, out value);
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryParseDouble(this ReadOnlySpan<char> span, NumberStyles style, IFormatProvider provider, out double value)
{
#if NETSTANDARD2_0
return double.TryParse(span.ToString(), style, provider, out value);
#else
return double.TryParse(span, style, provider, out value);
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryParseByte(this ReadOnlySpan<char> span, NumberStyles style, IFormatProvider provider, out byte value)
{
#if NETSTANDARD2_0
return byte.TryParse(span.ToString(), style, provider, out value);
#else
return byte.TryParse(span, style, provider, out value);
#endif
}
}
}

5
src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs

@ -37,8 +37,7 @@ namespace Avalonia.Build.Tasks
var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input, var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input,
File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(),
ProjectDirectory, OutputPath, VerifyIl, outputImportance, ProjectDirectory, OutputPath, VerifyIl, outputImportance,
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, SkipXamlCompilation, DebuggerLaunch);
EnableComInteropPatching, SkipXamlCompilation, DebuggerLaunch);
if (!res.Success) if (!res.Success)
return false; return false;
if (!res.WrittenFile) if (!res.WrittenFile)
@ -72,8 +71,6 @@ namespace Avalonia.Build.Tasks
public string OutputPath { get; set; } public string OutputPath { get; set; }
public bool VerifyIl { get; set; } public bool VerifyIl { get; set; }
public bool EnableComInteropPatching { get; set; }
public bool SkipXamlCompilation { get; set; } public bool SkipXamlCompilation { get; set; }
public string AssemblyOriginatorKeyFile { get; set; } public string AssemblyOriginatorKeyFile { get; set; }

6
src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs

@ -18,8 +18,6 @@ namespace Avalonia.Build.Tasks
public string Root { get; set; } public string Root { get; set; }
[Required] [Required]
public string Output { get; set; } public string Output { get; set; }
[Required]
public ITaskItem[] EmbeddedResources { get; set; }
public string ReportImportance { get; set; } public string ReportImportance { get; set; }
@ -148,10 +146,6 @@ namespace Avalonia.Build.Tasks
Enum.TryParse<MessageImportance>(ReportImportance, out _reportImportance); Enum.TryParse<MessageImportance>(ReportImportance, out _reportImportance);
BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance); BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance);
foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml") || r.ItemSpec.EndsWith(".axaml")))
BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec,
"XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work");
var resources = BuildResourceSources(); var resources = BuildResourceSources();
if (!PreProcessXamlFiles(resources)) if (!PreProcessXamlFiles(resources))

3
src/Avalonia.Build.Tasks/Program.cs

@ -44,8 +44,7 @@ namespace Avalonia.Build.Tasks
OutputPath = args[2], OutputPath = args[2],
BuildEngine = new ConsoleBuildEngine(), BuildEngine = new ConsoleBuildEngine(),
ProjectDirectory = Directory.GetCurrentDirectory(), ProjectDirectory = Directory.GetCurrentDirectory(),
VerifyIl = true, VerifyIl = true
EnableComInteropPatching = true
}.Execute() ? }.Execute() ?
0 : 0 :
2; 2;

72
src/Avalonia.Build.Tasks/SpanCompat.cs

@ -1,4 +1,7 @@
#if !NETCOREAPP3_1_OR_GREATER #if !NETCOREAPP3_1_OR_GREATER
using System.Globalization;
using System.Runtime.CompilerServices;
namespace System namespace System
{ {
// This is a hack to enable our span code to work inside MSBuild task without referencing System.Memory // This is a hack to enable our span code to work inside MSBuild task without referencing System.Memory
@ -9,6 +12,8 @@ namespace System
private int _length; private int _length;
public int Length => _length; public int Length => _length;
public static implicit operator ReadOnlySpan<T>(string s) => new ReadOnlySpan<T>(s);
public ReadOnlySpan(string s) : this(s, 0, s.Length) public ReadOnlySpan(string s) : this(s, 0, s.Length)
{ {
@ -63,8 +68,75 @@ namespace System
return Slice(start); return Slice(start);
} }
public ReadOnlySpan<char> TrimEnd()
{
int end = Length - 1;
for (; end >= 0; end--)
{
if (!char.IsWhiteSpace(this[end]))
{
break;
}
}
return Slice(0, end + 1);
}
public ReadOnlySpan<char> Trim()
{
return TrimStart().TrimEnd();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryParseUInt(NumberStyles style, IFormatProvider provider, out uint value)
{
return uint.TryParse(ToString(), style, provider, out value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryParseInt(out int value)
{
return int.TryParse(ToString(), out value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryParseDouble(NumberStyles style, IFormatProvider provider, out double value)
{
return double.TryParse(ToString(), style, provider, out value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryParseByte(NumberStyles style, IFormatProvider provider, out byte value)
{
return byte.TryParse(ToString(), style, provider, out value);
}
public override string ToString() => _length == 0 ? string.Empty : _s.Substring(_start, _length); public override string ToString() => _length == 0 ? string.Empty : _s.Substring(_start, _length);
internal int IndexOf(ReadOnlySpan<char> v, StringComparison ordinal, int start = 0)
{
if(Length == 0 || v.IsEmpty)
{
return -1;
}
for (var c = start; c < _length; c++)
{
if (this[c] == v[0])
{
for(var i = 0; i < v.Length; i++)
{
if (this[c + i] != v[i])
{
break;
}
}
return c;
}
}
return -1;
}
public static implicit operator ReadOnlySpan<T>(char[] arr) => new ReadOnlySpan<T>(new string(arr)); public static implicit operator ReadOnlySpan<T>(char[] arr) => new ReadOnlySpan<T>(new string(arr));
} }

32
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs

@ -24,38 +24,6 @@ namespace Avalonia.Build.Tasks
string Name { get; } string Name { get; }
IEnumerable<IResource> Resources { get; } IEnumerable<IResource> Resources { get; }
} }
class EmbeddedResources : IResourceGroup
{
private readonly AssemblyDefinition _asm;
public string Name => "EmbeddedResource";
public IEnumerable<IResource> Resources => _asm.MainModule.Resources.OfType<EmbeddedResource>()
.Select(r => new WrappedResource(_asm, r)).ToList();
public EmbeddedResources(AssemblyDefinition asm)
{
_asm = asm;
}
class WrappedResource : IResource
{
private readonly AssemblyDefinition _asm;
private readonly EmbeddedResource _res;
public WrappedResource(AssemblyDefinition asm, EmbeddedResource res)
{
_asm = asm;
_res = res;
}
public string Uri => $"resm:{Name}?assembly={_asm.Name.Name}";
public string Name => _res.Name;
public string FilePath => Name;
public byte[] FileContents => _res.GetResourceData();
public void Remove() => _asm.MainModule.Resources.Remove(_res);
}
}
class AvaloniaResources : IResourceGroup class AvaloniaResources : IResourceGroup
{ {

17
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@ -39,15 +39,15 @@ namespace Avalonia.Build.Tasks
public static CompileResult Compile(IBuildEngine engine, string input, string[] references, public static CompileResult Compile(IBuildEngine engine, string input, string[] references,
string projectDirectory, string projectDirectory,
string output, bool verifyIl, MessageImportance logImportance, string strongNameKey, bool patchCom, string output, bool verifyIl, MessageImportance logImportance, string strongNameKey,
bool skipXamlCompilation) bool skipXamlCompilation)
{ {
return Compile(engine, input, references, projectDirectory, output, verifyIl, logImportance, strongNameKey, patchCom, skipXamlCompilation, debuggerLaunch:false); return Compile(engine, input, references, projectDirectory, output, verifyIl, logImportance, strongNameKey, skipXamlCompilation, debuggerLaunch:false);
} }
internal static CompileResult Compile(IBuildEngine engine, string input, string[] references, internal static CompileResult Compile(IBuildEngine engine, string input, string[] references,
string projectDirectory, string projectDirectory,
string output, bool verifyIl, MessageImportance logImportance, string strongNameKey, bool patchCom, bool skipXamlCompilation, bool debuggerLaunch) string output, bool verifyIl, MessageImportance logImportance, string strongNameKey, bool skipXamlCompilation, bool debuggerLaunch)
{ {
var typeSystem = new CecilTypeSystem( var typeSystem = new CecilTypeSystem(
references.Where(r => !r.ToLowerInvariant().EndsWith("avalonia.build.tasks.dll")), references.Where(r => !r.ToLowerInvariant().EndsWith("avalonia.build.tasks.dll")),
@ -58,15 +58,12 @@ namespace Avalonia.Build.Tasks
if (!skipXamlCompilation) if (!skipXamlCompilation)
{ {
var compileRes = CompileCore(engine, typeSystem, projectDirectory, verifyIl, logImportance, debuggerLaunch); var compileRes = CompileCore(engine, typeSystem, projectDirectory, verifyIl, logImportance, debuggerLaunch);
if (compileRes == null && !patchCom) if (compileRes == null)
return new CompileResult(true); return new CompileResult(true);
if (compileRes == false) if (compileRes == false)
return new CompileResult(false); return new CompileResult(false);
} }
if (patchCom)
ComInteropHelper.PatchAssembly(asm, typeSystem);
var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols }; var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols };
if (!string.IsNullOrWhiteSpace(strongNameKey)) if (!string.IsNullOrWhiteSpace(strongNameKey))
writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey); writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey);
@ -112,9 +109,8 @@ namespace Avalonia.Build.Tasks
} }
} }
var asm = typeSystem.TargetAssemblyDefinition; var asm = typeSystem.TargetAssemblyDefinition;
var emres = new EmbeddedResources(asm);
var avares = new AvaloniaResources(asm, projectDirectory); var avares = new AvaloniaResources(asm, projectDirectory);
if (avares.Resources.Count(CheckXamlName) == 0 && emres.Resources.Count(CheckXamlName) == 0) if (avares.Resources.Count(CheckXamlName) == 0)
// Nothing to do // Nothing to do
return null; return null;
@ -436,9 +432,6 @@ namespace Avalonia.Build.Tasks
return true; return true;
} }
if (emres.Resources.Count(CheckXamlName) != 0)
if (!CompileGroup(emres))
return false;
if (avares.Resources.Count(CheckXamlName) != 0) if (avares.Resources.Count(CheckXamlName) != 0)
{ {
if (!CompileGroup(avares)) if (!CompileGroup(avares))

83
src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs

@ -42,6 +42,11 @@ namespace Avalonia.Controls
Red800 = 0xFFC62828, Red800 = 0xFFC62828,
Red900 = 0xFFB71C1C, Red900 = 0xFFB71C1C,
RedA100 = 0xFFFF8A80,
RedA200 = 0xFFFF5252,
RedA400 = 0xFFFF1744,
RedA700 = 0xFFD50000,
// Pink // Pink
Pink50 = 0xFFFCE4EC, Pink50 = 0xFFFCE4EC,
Pink100 = 0xFFF8BBD0, Pink100 = 0xFFF8BBD0,
@ -54,6 +59,11 @@ namespace Avalonia.Controls
Pink800 = 0xFFAD1457, Pink800 = 0xFFAD1457,
Pink900 = 0xFF880E4F, Pink900 = 0xFF880E4F,
PinkA100 = 0xFFFF80AB,
PinkA200 = 0xFFFF4081,
PinkA400 = 0xFFF50057,
PinkA700 = 0xFFC51162,
// Purple // Purple
Purple50 = 0xFFF3E5F5, Purple50 = 0xFFF3E5F5,
Purple100 = 0xFFE1BEE7, Purple100 = 0xFFE1BEE7,
@ -66,6 +76,11 @@ namespace Avalonia.Controls
Purple800 = 0xFF6A1B9A, Purple800 = 0xFF6A1B9A,
Purple900 = 0xFF4A148C, Purple900 = 0xFF4A148C,
PurpleA100 = 0xFFEA80FC,
PurpleA200 = 0xFFE040FB,
PurpleA400 = 0xFFD500F9,
PurpleA700 = 0xFFAA00FF,
// Deep Purple // Deep Purple
DeepPurple50 = 0xFFEDE7F6, DeepPurple50 = 0xFFEDE7F6,
DeepPurple100 = 0xFFD1C4E9, DeepPurple100 = 0xFFD1C4E9,
@ -78,6 +93,11 @@ namespace Avalonia.Controls
DeepPurple800 = 0xFF4527A0, DeepPurple800 = 0xFF4527A0,
DeepPurple900 = 0xFF311B92, DeepPurple900 = 0xFF311B92,
DeepPurpleA100 = 0xFFB388FF,
DeepPurpleA200 = 0xFF7C4DFF,
DeepPurpleA400 = 0xFF651FFF,
DeepPurpleA700 = 0xFF6200EA,
// Indigo // Indigo
Indigo50 = 0xFFE8EAF6, Indigo50 = 0xFFE8EAF6,
Indigo100 = 0xFFC5CAE9, Indigo100 = 0xFFC5CAE9,
@ -90,6 +110,11 @@ namespace Avalonia.Controls
Indigo800 = 0xFF283593, Indigo800 = 0xFF283593,
Indigo900 = 0xFF1A237E, Indigo900 = 0xFF1A237E,
IndigoA100 = 0xFF8C9EFF,
IndigoA200 = 0xFF536DFE,
IndigoA400 = 0xFF3D5AFE,
IndigoA700 = 0xFF304FFE,
// Blue // Blue
Blue50 = 0xFFE3F2FD, Blue50 = 0xFFE3F2FD,
Blue100 = 0xFFBBDEFB, Blue100 = 0xFFBBDEFB,
@ -102,6 +127,11 @@ namespace Avalonia.Controls
Blue800 = 0xFF1565C0, Blue800 = 0xFF1565C0,
Blue900 = 0xFF0D47A1, Blue900 = 0xFF0D47A1,
BlueA100 = 0xFF82B1FF,
BlueA200 = 0xFF448AFF,
BlueA400 = 0xFF2979FF,
BlueA700 = 0xFF2962FF,
// Light Blue // Light Blue
LightBlue50 = 0xFFE1F5FE, LightBlue50 = 0xFFE1F5FE,
LightBlue100 = 0xFFB3E5FC, LightBlue100 = 0xFFB3E5FC,
@ -114,6 +144,11 @@ namespace Avalonia.Controls
LightBlue800 = 0xFF0277BD, LightBlue800 = 0xFF0277BD,
LightBlue900 = 0xFF01579B, LightBlue900 = 0xFF01579B,
LightBlueA100 = 0xFF80D8FF,
LightBlueA200 = 0xFF40C4FF,
LightBlueA400 = 0xFF00B0FF,
LightBlueA700 = 0xFF0091EA,
// Cyan // Cyan
Cyan50 = 0xFFE0F7FA, Cyan50 = 0xFFE0F7FA,
Cyan100 = 0xFFB2EBF2, Cyan100 = 0xFFB2EBF2,
@ -126,6 +161,11 @@ namespace Avalonia.Controls
Cyan800 = 0xFF00838F, Cyan800 = 0xFF00838F,
Cyan900 = 0xFF006064, Cyan900 = 0xFF006064,
CyanA100 = 0xFF84FFFF,
CyanA200 = 0xFF18FFFF,
CyanA400 = 0xFF00E5FF,
CyanA700 = 0xFF00B8D4,
// Teal // Teal
Teal50 = 0xFFE0F2F1, Teal50 = 0xFFE0F2F1,
Teal100 = 0xFFB2DFDB, Teal100 = 0xFFB2DFDB,
@ -138,6 +178,11 @@ namespace Avalonia.Controls
Teal800 = 0xFF00695C, Teal800 = 0xFF00695C,
Teal900 = 0xFF004D40, Teal900 = 0xFF004D40,
TealA100 = 0xFFA7FFEB,
TealA200 = 0xFF64FFDA,
TealA400 = 0xFF1DE9B6,
TealA700 = 0xFF00BFA5,
// Green // Green
Green50 = 0xFFE8F5E9, Green50 = 0xFFE8F5E9,
Green100 = 0xFFC8E6C9, Green100 = 0xFFC8E6C9,
@ -150,6 +195,11 @@ namespace Avalonia.Controls
Green800 = 0xFF2E7D32, Green800 = 0xFF2E7D32,
Green900 = 0xFF1B5E20, Green900 = 0xFF1B5E20,
GreenA100 = 0xFFB9F6CA,
GreenA200 = 0xFF69F0AE,
GreenA400 = 0xFF00E676,
GreenA700 = 0xFF00C853,
// Light Green // Light Green
LightGreen50 = 0xFFF1F8E9, LightGreen50 = 0xFFF1F8E9,
LightGreen100 = 0xFFDCEDC8, LightGreen100 = 0xFFDCEDC8,
@ -162,6 +212,11 @@ namespace Avalonia.Controls
LightGreen800 = 0xFF558B2F, LightGreen800 = 0xFF558B2F,
LightGreen900 = 0xFF33691E, LightGreen900 = 0xFF33691E,
LightGreenA100 = 0xFFCCFF90,
LightGreenA200 = 0xFFB2FF59,
LightGreenA400 = 0xFF76FF03,
LightGreenA700 = 0xFF64DD17,
// Lime // Lime
Lime50 = 0xFFF9FBE7, Lime50 = 0xFFF9FBE7,
Lime100 = 0xFFF0F4C3, Lime100 = 0xFFF0F4C3,
@ -174,6 +229,11 @@ namespace Avalonia.Controls
Lime800 = 0xFF9E9D24, Lime800 = 0xFF9E9D24,
Lime900 = 0xFF827717, Lime900 = 0xFF827717,
LimeA100 = 0xFFF4FF81,
LimeA200 = 0xFFEEFF41,
LimeA400 = 0xFFC6FF00,
LimeA700 = 0xFFAEEA00,
// Yellow // Yellow
Yellow50 = 0xFFFFFDE7, Yellow50 = 0xFFFFFDE7,
Yellow100 = 0xFFFFF9C4, Yellow100 = 0xFFFFF9C4,
@ -186,6 +246,11 @@ namespace Avalonia.Controls
Yellow800 = 0xFFF9A825, Yellow800 = 0xFFF9A825,
Yellow900 = 0xFFF57F17, Yellow900 = 0xFFF57F17,
YellowA100 = 0xFFFFFF8D,
YellowA200 = 0xFFFFFF00,
YellowA400 = 0xFFFFEA00,
YellowA700 = 0xFFFFD600,
// Amber // Amber
Amber50 = 0xFFFFF8E1, Amber50 = 0xFFFFF8E1,
Amber100 = 0xFFFFECB3, Amber100 = 0xFFFFECB3,
@ -198,6 +263,11 @@ namespace Avalonia.Controls
Amber800 = 0xFFFF8F00, Amber800 = 0xFFFF8F00,
Amber900 = 0xFFFF6F00, Amber900 = 0xFFFF6F00,
AmberA100 = 0xFFFFE57F,
AmberA200 = 0xFFFFD740,
AmberA400 = 0xFFFFC400,
AmberA700 = 0xFFFFAB00,
// Orange // Orange
Orange50 = 0xFFFFF3E0, Orange50 = 0xFFFFF3E0,
Orange100 = 0xFFFFE0B2, Orange100 = 0xFFFFE0B2,
@ -210,6 +280,11 @@ namespace Avalonia.Controls
Orange800 = 0xFFEF6C00, Orange800 = 0xFFEF6C00,
Orange900 = 0xFFE65100, Orange900 = 0xFFE65100,
OrangeA100 = 0xFFFFD180,
OrangeA200 = 0xFFFFAB40,
OrangeA400 = 0xFFFF9100,
OrangeA700 = 0xFFFF6D00,
// Deep Orange // Deep Orange
DeepOrange50 = 0xFFFBE9E7, DeepOrange50 = 0xFFFBE9E7,
DeepOrange100 = 0xFFFFCCBC, DeepOrange100 = 0xFFFFCCBC,
@ -222,6 +297,11 @@ namespace Avalonia.Controls
DeepOrange800 = 0xFFD84315, DeepOrange800 = 0xFFD84315,
DeepOrange900 = 0xFFBF360C, DeepOrange900 = 0xFFBF360C,
DeepOrangeA100 = 0xFFFF9E80,
DeepOrangeA200 = 0xFFFF6E40,
DeepOrangeA400 = 0xFFFF3D00,
DeepOrangeA700 = 0xFFDD2C00,
// Brown // Brown
Brown50 = 0xFFEFEBE9, Brown50 = 0xFFEFEBE9,
Brown100 = 0xFFD7CCC8, Brown100 = 0xFFD7CCC8,
@ -257,6 +337,9 @@ namespace Avalonia.Controls
BlueGray700 = 0xFF455A64, BlueGray700 = 0xFF455A64,
BlueGray800 = 0xFF37474F, BlueGray800 = 0xFF37474F,
BlueGray900 = 0xFF263238, BlueGray900 = 0xFF263238,
Black = 0xFF000000,
White = 0xFFFFFFFF,
} }
// See: https://material.io/design/color/the-color-system.html#tools-for-picking-colors // See: https://material.io/design/color/the-color-system.html#tools-for-picking-colors

28
src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs

@ -14,33 +14,5 @@ namespace Avalonia.Controls
public ColorPicker() : base() public ColorPicker() : base()
{ {
} }
/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
// Until this point the ColorPicker itself is responsible to process property updates.
// This, for example, syncs Color with HsvColor and updates primitive controls.
//
// However, when the template is created, hand-off this change processing to the
// ColorView within the control template itself. Remember ColorPicker derives from
// ColorView so we don't want two instances of the same logic fighting each other.
// It is best to hand-off to the ColorView in the control template because that is the
// primary point of user-interaction for the overall control. It also simplifies binding.
//
// Keep in mind this hand-off is not possible until the template controls are created
// which is done after the ColorPicker is instantiated. The ColorPicker must still
// process updates before the template is applied to ensure all property changes in
// XAML or object initializers are handled correctly. Otherwise, there can be bugs
// such as setting the Color property doesn't work because the HsvColor is never updated
// and then the Color value is lost once the template loads (and the template ColorView
// takes over).
//
// In order to complete this hand-off, completely ignore property changes here in the
// ColorPicker. This means the ColorView in the control template is now responsible to
// process property changes and handle primary calculations.
base.ignorePropertyChanged = true;
}
} }
} }

2
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs

@ -240,7 +240,7 @@ namespace Avalonia.Controls.Primitives
public ColorComponent ThirdComponent public ColorComponent ThirdComponent
{ {
get => GetValue(ThirdComponentProperty); get => GetValue(ThirdComponentProperty);
private set => SetValue(ThirdComponentProperty, value); protected set => SetValue(ThirdComponentProperty, value);
} }
} }
} }

484
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@ -1,8 +1,15 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Avalonia.Controls" xmlns:controls="using:Avalonia.Controls"
xmlns:converters="using:Avalonia.Controls.Converters"
xmlns:primitives="using:Avalonia.Controls.Primitives"
xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker"
x:CompileBindings="True"> x:CompileBindings="True">
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml" />
</ResourceDictionary.MergedDictionaries>
<ControlTheme x:Key="{x:Type ColorPicker}" <ControlTheme x:Key="{x:Type ColorPicker}"
TargetType="ColorPicker"> TargetType="ColorPicker">
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" /> <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
@ -43,39 +50,454 @@
</DropDownButton.Content> </DropDownButton.Content>
<DropDownButton.Flyout> <DropDownButton.Flyout>
<Flyout FlyoutPresenterClasses="nopadding"> <Flyout FlyoutPresenterClasses="nopadding">
<ColorView x:Name="FlyoutColorView"
Color="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" <!-- The following is copy-pasted from the ColorView's control template.
ColorModel="{Binding ColorModel, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" It MUST always be kept in sync with the ColorView (which is master).
ColorSpectrumComponents="{TemplateBinding ColorSpectrumComponents}" Note the only changes are resources specific to the ColorPicker. -->
ColorSpectrumShape="{TemplateBinding ColorSpectrumShape}" <Grid RowDefinitions="Auto,Auto">
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}" <Grid.Resources>
IsAlphaEnabled="{TemplateBinding IsAlphaEnabled}"
IsAlphaVisible="{TemplateBinding IsAlphaVisible}"
IsColorComponentsVisible="{TemplateBinding IsColorComponentsVisible}"
IsColorModelVisible="{TemplateBinding IsColorModelVisible}"
IsColorPaletteVisible="{TemplateBinding IsColorPaletteVisible}"
IsColorPreviewVisible="{TemplateBinding IsColorPreviewVisible}"
IsColorSpectrumVisible="{TemplateBinding IsColorSpectrumVisible}"
IsColorSpectrumSliderVisible="{TemplateBinding IsColorSpectrumSliderVisible}"
IsComponentSliderVisible="{TemplateBinding IsComponentSliderVisible}"
IsComponentTextInputVisible="{TemplateBinding IsComponentTextInputVisible}"
IsHexInputVisible="{TemplateBinding IsHexInputVisible}"
MaxHue="{TemplateBinding MaxHue}"
MaxSaturation="{TemplateBinding MaxSaturation}"
MaxValue="{TemplateBinding MaxValue}"
MinHue="{TemplateBinding MinHue}"
MinSaturation="{TemplateBinding MinSaturation}"
MinValue="{TemplateBinding MinValue}"
PaletteColors="{TemplateBinding PaletteColors}"
PaletteColumnCount="{TemplateBinding PaletteColumnCount}"
Palette="{TemplateBinding Palette}"
SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<ColorView.Resources>
<!-- This radius must follow OverlayCornerRadius --> <!-- This radius must follow OverlayCornerRadius -->
<CornerRadius x:Key="ColorViewTabBackgroundCornerRadius">5,5,0,0</CornerRadius> <CornerRadius x:Key="ColorViewTabBackgroundCornerRadius">5,5,0,0</CornerRadius>
</ColorView.Resources> </Grid.Resources>
</ColorView>
<!-- Backgrounds -->
<!-- TODO: Background="{DynamicResource ColorViewTabBackgroundBrush}" -->
<Border x:Name="TabBackgroundBorder"
Grid.Row="0"
Grid.RowSpan="2"
Height="48"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
BorderBrush="{DynamicResource ColorViewTabBorderBrush}"
CornerRadius="{DynamicResource ColorViewTabBackgroundCornerRadius}" />
<Border x:Name="ContentBackgroundBorder"
Grid.Row="0"
Grid.RowSpan="2"
Margin="0,48,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource BottomCornerRadiusFilterConverter}}"
Background="{DynamicResource ColorViewContentBackgroundBrush}"
BorderBrush="{DynamicResource ColorViewContentBorderBrush}"
BorderThickness="0,1,0,0" />
<TabControl x:Name="PART_TabControl"
Grid.Row="0"
Height="338"
Width="350"
Padding="0"
SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<TabControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="0"
Rows="1" />
</ItemsPanelTemplate>
</TabControl.ItemsPanel>
<!-- Spectrum Tab -->
<TabItem Theme="{StaticResource ColorViewTabItemTheme}"
IsVisible="{TemplateBinding IsColorSpectrumVisible}">
<TabItem.Header>
<Border Height="{DynamicResource ColorViewTabStripHeight}">
<PathIcon Width="20"
Height="20"
Data="{DynamicResource ColorViewSpectrumIconGeometry}" />
</Border>
</TabItem.Header>
<Grid RowDefinitions="*"
Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
MinWidth="32" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"
MinWidth="32" />
</Grid.ColumnDefinitions>
<primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
AutomationProperties.Name="Third Component"
Grid.Column="0"
IsAlphaMaxForced="True"
IsSaturationValueMaxForced="False"
Orientation="Vertical"
ColorModel="Hsva"
ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Margin="0,0,12,0"
IsVisible="{TemplateBinding IsColorSpectrumSliderVisible}" />
<primitives:ColorSpectrum x:Name="ColorSpectrum"
Grid.Column="1"
Components="{TemplateBinding ColorSpectrumComponents}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
MinHue="{TemplateBinding MinHue}"
MaxHue="{TemplateBinding MaxHue}"
MinSaturation="{TemplateBinding MinSaturation}"
MaxSaturation="{TemplateBinding MaxSaturation}"
MinValue="{TemplateBinding MinValue}"
MaxValue="{TemplateBinding MaxValue}"
Shape="{TemplateBinding ColorSpectrumShape}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<primitives:ColorSlider x:Name="ColorSpectrumAlphaSlider"
AutomationProperties.Name="Alpha Component"
Grid.Column="2"
Orientation="Vertical"
ColorModel="Hsva"
ColorComponent="Alpha"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Margin="12,0,0,0"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<primitives:ColorSlider.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
</MultiBinding>
</primitives:ColorSlider.IsVisible>
</primitives:ColorSlider>
</Grid>
</TabItem>
<!-- Palette Tab -->
<TabItem Theme="{StaticResource ColorViewTabItemTheme}"
IsVisible="{TemplateBinding IsColorPaletteVisible}">
<TabItem.Header>
<Border Height="{DynamicResource ColorViewTabStripHeight}">
<PathIcon Width="20"
Height="20"
Data="{DynamicResource ColorViewPaletteIconGeometry}" />
</Border>
</TabItem.Header>
<ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
Items="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False"
Margin="12">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type Color}">
<Border AutomationProperties.Name="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
ToolTip.Tip="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border.Background>
<SolidColorBrush Color="{Binding}" />
</Border.Background>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding $parent[ColorView].PaletteColumnCount}" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</TabItem>
<!-- Components Tab -->
<TabItem Theme="{StaticResource ColorViewTabItemTheme}"
IsVisible="{TemplateBinding IsColorComponentsVisible}">
<TabItem.Header>
<Border Height="{DynamicResource ColorViewTabStripHeight}">
<PathIcon Width="20"
Height="20"
Data="{DynamicResource ColorViewComponentsIconGeometry}" />
</Border>
</TabItem.Header>
<Grid ColumnDefinitions="Auto,Auto,*"
RowDefinitions="Auto,24,1*,1*,1*,1*,12"
Margin="12">
<!-- Top color model & Hex input -->
<Grid Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="0"
ColumnDefinitions="1*,12,1*">
<!-- Content RGB/HSV names are hard-coded and considered universal -->
<!-- RadioButtons are styled to look like a 'SegmentedControl' or 'ButtonGroup' -->
<Grid ColumnDefinitions="1*,1*"
IsVisible="{TemplateBinding IsColorModelVisible}">
<RadioButton x:Name="RgbRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
Grid.Column="0"
Content="RGB"
CornerRadius="4,0,0,4"
BorderThickness="1,1,0,1"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
<RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
Grid.Column="1"
Content="HSV"
CornerRadius="0,4,4,0"
BorderThickness="0,1,1,1"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
</Grid>
<Grid x:Name="HexInputGrid"
Grid.Column="2"
IsVisible="{TemplateBinding IsHexInputVisible}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
Height="32"
Background="{DynamicResource TextControlBackgroundDisabled}"
BorderBrush="{DynamicResource TextControlBorderBrush}"
BorderThickness="1,1,0,1"
CornerRadius="4,0,0,4">
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="#"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<!-- Color updated in code-behind -->
<TextBox x:Name="PART_HexTextBox"
Grid.Column="1"
AutomationProperties.Name="Hexadecimal Color"
Height="32"
MaxLength="8"
HorizontalAlignment="Stretch"
CornerRadius="0,4,4,0" />
</Grid>
</Grid>
<!-- Color component editing controls -->
<!-- Component 1 RGB:Red HSV:Hue -->
<Border Grid.Column="0"
Grid.Row="2"
Height="{Binding ElementName=Component1NumericUpDown, Path=Bounds.Height}"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Background="{DynamicResource TextControlBackgroundDisabled}"
BorderBrush="{DynamicResource TextControlBorderBrush}"
BorderThickness="1,1,0,1"
CornerRadius="4,0,0,4"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentTextInputVisible}">
<Panel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="R"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="H"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
</Panel>
</Border>
<NumericUpDown x:Name="Component1NumericUpDown"
Grid.Column="1"
Grid.Row="2"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,4,4,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=Component1Slider}"
Maximum="{Binding Maximum, ElementName=Component1Slider}"
Value="{Binding Value, ElementName=Component1Slider}"
IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
<primitives:ColorSlider x:Name="Component1Slider"
Grid.Column="2"
Grid.Row="2"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Component1"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentSliderVisible}" />
<!-- Component 2 RGB:Green HSV:Saturation -->
<Border Grid.Column="0"
Grid.Row="3"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Height="{Binding ElementName=Component2NumericUpDown, Path=Bounds.Height}"
Background="{DynamicResource TextControlBackgroundDisabled}"
BorderBrush="{DynamicResource TextControlBorderBrush}"
BorderThickness="1,1,0,1"
CornerRadius="4,0,0,4"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentTextInputVisible}">
<Panel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="G"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="S"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
</Panel>
</Border>
<NumericUpDown x:Name="Component2NumericUpDown"
Grid.Column="1"
Grid.Row="3"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,4,4,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=Component2Slider}"
Maximum="{Binding Maximum, ElementName=Component2Slider}"
Value="{Binding Value, ElementName=Component2Slider}"
IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
<primitives:ColorSlider x:Name="Component2Slider"
Grid.Column="2"
Grid.Row="3"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Component2"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentSliderVisible}" />
<!-- Component 3 RGB:Blue HSV:Value -->
<Border Grid.Column="0"
Grid.Row="4"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Height="{Binding ElementName=Component3NumericUpDown, Path=Bounds.Height}"
Background="{DynamicResource TextControlBackgroundDisabled}"
BorderBrush="{DynamicResource TextControlBorderBrush}"
BorderThickness="1,1,0,1"
CornerRadius="4,0,0,4"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentTextInputVisible}">
<Panel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="B"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="V"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
</Panel>
</Border>
<NumericUpDown x:Name="Component3NumericUpDown"
Grid.Column="1"
Grid.Row="4"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,4,4,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=Component3Slider}"
Maximum="{Binding Maximum, ElementName=Component3Slider}"
Value="{Binding Value, ElementName=Component3Slider}"
IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
<primitives:ColorSlider x:Name="Component3Slider"
Grid.Column="2"
Grid.Row="4"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Component3"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentSliderVisible}" />
<!-- Alpha Component -->
<Border Grid.Column="0"
Grid.Row="5"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Height="{Binding ElementName=AlphaComponentNumericUpDown, Path=Bounds.Height}"
Background="{DynamicResource TextControlBackgroundDisabled}"
BorderBrush="{DynamicResource TextControlBorderBrush}"
BorderThickness="1,1,0,1"
CornerRadius="4,0,0,4"
VerticalAlignment="Center"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<TextBlock x:Name="AlphaComponentTextBlock"
Foreground="{DynamicResource TextControlForegroundDisabled}"
FontWeight="SemiBold"
Text="A"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsComponentTextInputVisible" />
</MultiBinding>
</Border.IsVisible>
</Border>
<NumericUpDown x:Name="AlphaComponentNumericUpDown"
Grid.Column="1"
Grid.Row="5"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,4,4,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=AlphaComponentSlider}"
Maximum="{Binding Maximum, ElementName=AlphaComponentSlider}"
Value="{Binding Value, ElementName=AlphaComponentSlider}"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<NumericUpDown.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsComponentTextInputVisible" />
</MultiBinding>
</NumericUpDown.IsVisible>
</NumericUpDown>
<primitives:ColorSlider x:Name="AlphaComponentSlider"
Grid.Column="2"
Grid.Row="5"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Alpha"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<primitives:ColorSlider.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsComponentSliderVisible" />
</MultiBinding>
</primitives:ColorSlider.IsVisible>
</primitives:ColorSlider>
</Grid>
</TabItem>
</TabControl>
<!-- Previewer -->
<!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
<primitives:ColorPreviewer Grid.Row="1"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
Margin="12,-5,12,7"
IsVisible="{TemplateBinding IsColorPreviewVisible}" />
</Grid>
</Flyout> </Flyout>
</DropDownButton.Flyout> </DropDownButton.Flyout>
</DropDownButton> </DropDownButton>

486
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml

@ -1,8 +1,15 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Avalonia.Controls" xmlns:controls="using:Avalonia.Controls"
xmlns:converters="using:Avalonia.Controls.Converters"
xmlns:primitives="using:Avalonia.Controls.Primitives"
xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker"
x:CompileBindings="True"> x:CompileBindings="True">
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml" />
</ResourceDictionary.MergedDictionaries>
<ControlTheme x:Key="{x:Type ColorPicker}" <ControlTheme x:Key="{x:Type ColorPicker}"
TargetType="ColorPicker"> TargetType="ColorPicker">
<Setter Property="CornerRadius" Value="0" /> <Setter Property="CornerRadius" Value="0" />
@ -42,40 +49,455 @@
</Panel> </Panel>
</DropDownButton.Content> </DropDownButton.Content>
<DropDownButton.Flyout> <DropDownButton.Flyout>
<Flyout FlyoutPresenterClasses="nopadding"> <Flyout>
<ColorView x:Name="FlyoutColorView"
Color="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" <!-- The following is copy-pasted from the ColorView's control template.
ColorModel="{Binding ColorModel, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" It MUST always be kept in sync with the ColorView (which is master).
ColorSpectrumComponents="{TemplateBinding ColorSpectrumComponents}" Note the only changes are resources specific to the ColorPicker. -->
ColorSpectrumShape="{TemplateBinding ColorSpectrumShape}" <Grid RowDefinitions="Auto,Auto">
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}" <Grid.Resources>
IsAlphaEnabled="{TemplateBinding IsAlphaEnabled}"
IsAlphaVisible="{TemplateBinding IsAlphaVisible}"
IsColorComponentsVisible="{TemplateBinding IsColorComponentsVisible}"
IsColorModelVisible="{TemplateBinding IsColorModelVisible}"
IsColorPaletteVisible="{TemplateBinding IsColorPaletteVisible}"
IsColorPreviewVisible="{TemplateBinding IsColorPreviewVisible}"
IsColorSpectrumVisible="{TemplateBinding IsColorSpectrumVisible}"
IsColorSpectrumSliderVisible="{TemplateBinding IsColorSpectrumSliderVisible}"
IsComponentSliderVisible="{TemplateBinding IsComponentSliderVisible}"
IsComponentTextInputVisible="{TemplateBinding IsComponentTextInputVisible}"
IsHexInputVisible="{TemplateBinding IsHexInputVisible}"
MaxHue="{TemplateBinding MaxHue}"
MaxSaturation="{TemplateBinding MaxSaturation}"
MaxValue="{TemplateBinding MaxValue}"
MinHue="{TemplateBinding MinHue}"
MinSaturation="{TemplateBinding MinSaturation}"
MinValue="{TemplateBinding MinValue}"
PaletteColors="{TemplateBinding PaletteColors}"
PaletteColumnCount="{TemplateBinding PaletteColumnCount}"
Palette="{TemplateBinding Palette}"
SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<ColorView.Resources>
<!-- This radius must follow OverlayCornerRadius --> <!-- This radius must follow OverlayCornerRadius -->
<CornerRadius x:Key="ColorViewTabBackgroundCornerRadius">0,0,0,0</CornerRadius> <CornerRadius x:Key="ColorViewTabBackgroundCornerRadius">0,0,0,0</CornerRadius>
</ColorView.Resources> </Grid.Resources>
</ColorView>
<!-- Backgrounds -->
<!-- TODO: Background="{DynamicResource ColorViewTabBackgroundBrush}" -->
<Border x:Name="TabBackgroundBorder"
Grid.Row="0"
Grid.RowSpan="2"
Height="48"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
BorderBrush="{DynamicResource ColorViewTabBorderBrush}"
CornerRadius="{DynamicResource ColorViewTabBackgroundCornerRadius}" />
<Border x:Name="ContentBackgroundBorder"
Grid.Row="0"
Grid.RowSpan="2"
Margin="0,48,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource BottomCornerRadiusFilterConverter}}"
Background="{DynamicResource ColorViewContentBackgroundBrush}"
BorderBrush="{DynamicResource ColorViewContentBorderBrush}"
BorderThickness="0,1,0,0" />
<TabControl x:Name="PART_TabControl"
Grid.Row="0"
Height="338"
Width="350"
Padding="0"
SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<TabControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="0"
Rows="1" />
</ItemsPanelTemplate>
</TabControl.ItemsPanel>
<!-- Spectrum Tab -->
<TabItem Theme="{StaticResource ColorViewTabItemTheme}"
IsVisible="{TemplateBinding IsColorSpectrumVisible}">
<TabItem.Header>
<Border Height="{DynamicResource ColorViewTabStripHeight}">
<PathIcon Width="20"
Height="20"
Data="{DynamicResource ColorViewSpectrumIconGeometry}" />
</Border>
</TabItem.Header>
<Grid RowDefinitions="*"
Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
MinWidth="32" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"
MinWidth="32" />
</Grid.ColumnDefinitions>
<primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
AutomationProperties.Name="Third Component"
Grid.Column="0"
IsAlphaMaxForced="True"
IsSaturationValueMaxForced="False"
Orientation="Vertical"
ColorModel="Hsva"
ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Margin="0,0,12,0"
IsVisible="{TemplateBinding IsColorSpectrumSliderVisible}" />
<primitives:ColorSpectrum x:Name="ColorSpectrum"
Grid.Column="1"
Components="{TemplateBinding ColorSpectrumComponents}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
MinHue="{TemplateBinding MinHue}"
MaxHue="{TemplateBinding MaxHue}"
MinSaturation="{TemplateBinding MinSaturation}"
MaxSaturation="{TemplateBinding MaxSaturation}"
MinValue="{TemplateBinding MinValue}"
MaxValue="{TemplateBinding MaxValue}"
Shape="{TemplateBinding ColorSpectrumShape}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<primitives:ColorSlider x:Name="ColorSpectrumAlphaSlider"
AutomationProperties.Name="Alpha Component"
Grid.Column="2"
Orientation="Vertical"
ColorModel="Hsva"
ColorComponent="Alpha"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Margin="12,0,0,0"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<primitives:ColorSlider.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
</MultiBinding>
</primitives:ColorSlider.IsVisible>
</primitives:ColorSlider>
</Grid>
</TabItem>
<!-- Palette Tab -->
<TabItem Theme="{StaticResource ColorViewTabItemTheme}"
IsVisible="{TemplateBinding IsColorPaletteVisible}">
<TabItem.Header>
<Border Height="{DynamicResource ColorViewTabStripHeight}">
<PathIcon Width="20"
Height="20"
Data="{DynamicResource ColorViewPaletteIconGeometry}" />
</Border>
</TabItem.Header>
<ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
Items="{TemplateBinding PaletteColors}"
SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
UseLayoutRounding="False"
Margin="12">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type Color}">
<Border AutomationProperties.Name="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
ToolTip.Tip="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border.Background>
<SolidColorBrush Color="{Binding}" />
</Border.Background>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding $parent[ColorView].PaletteColumnCount}" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</TabItem>
<!-- Components Tab -->
<TabItem Theme="{StaticResource ColorViewTabItemTheme}"
IsVisible="{TemplateBinding IsColorComponentsVisible}">
<TabItem.Header>
<Border Height="{DynamicResource ColorViewTabStripHeight}">
<PathIcon Width="20"
Height="20"
Data="{DynamicResource ColorViewComponentsIconGeometry}" />
</Border>
</TabItem.Header>
<Grid ColumnDefinitions="Auto,Auto,*"
RowDefinitions="Auto,24,1*,1*,1*,1*,12"
Margin="12">
<!-- Top color model & Hex input -->
<Grid Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="0"
ColumnDefinitions="1*,12,1*">
<!-- Content RGB/HSV names are hard-coded and considered universal -->
<!-- RadioButtons are styled to look like a 'SegmentedControl' or 'ButtonGroup' -->
<Grid ColumnDefinitions="1*,1*"
IsVisible="{TemplateBinding IsColorModelVisible}">
<RadioButton x:Name="RgbRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
Grid.Column="0"
Content="RGB"
CornerRadius="0,0,0,0"
BorderThickness="1,1,0,1"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
<RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
Grid.Column="1"
Content="HSV"
CornerRadius="0,0,0,0"
BorderThickness="0,1,1,1"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
</Grid>
<Grid x:Name="HexInputGrid"
Grid.Column="2"
IsVisible="{TemplateBinding IsHexInputVisible}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
Height="32"
Background="{DynamicResource ThemeControlMidBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1,1,0,1"
CornerRadius="0,0,0,0">
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="#"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<!-- Color updated in code-behind -->
<TextBox x:Name="PART_HexTextBox"
Grid.Column="1"
AutomationProperties.Name="Hexadecimal Color"
Height="32"
MaxLength="8"
HorizontalAlignment="Stretch"
CornerRadius="0,0,0,0" />
</Grid>
</Grid>
<!-- Color component editing controls -->
<!-- Component 1 RGB:Red HSV:Hue -->
<Border Grid.Column="0"
Grid.Row="2"
Height="{Binding ElementName=Component1NumericUpDown, Path=Bounds.Height}"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Background="{DynamicResource ThemeControlMidBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1,1,0,1"
CornerRadius="0,0,0,0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentTextInputVisible}">
<Panel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="R"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="H"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
</Panel>
</Border>
<NumericUpDown x:Name="Component1NumericUpDown"
Grid.Column="1"
Grid.Row="2"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,0,0,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=Component1Slider}"
Maximum="{Binding Maximum, ElementName=Component1Slider}"
Value="{Binding Value, ElementName=Component1Slider}"
IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
<primitives:ColorSlider x:Name="Component1Slider"
Grid.Column="2"
Grid.Row="2"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Component1"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentSliderVisible}" />
<!-- Component 2 RGB:Green HSV:Saturation -->
<Border Grid.Column="0"
Grid.Row="3"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Height="{Binding ElementName=Component2NumericUpDown, Path=Bounds.Height}"
Background="{DynamicResource ThemeControlMidBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1,1,0,1"
CornerRadius="0,0,0,0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentTextInputVisible}">
<Panel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="G"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="S"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
</Panel>
</Border>
<NumericUpDown x:Name="Component2NumericUpDown"
Grid.Column="1"
Grid.Row="3"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,0,0,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=Component2Slider}"
Maximum="{Binding Maximum, ElementName=Component2Slider}"
Value="{Binding Value, ElementName=Component2Slider}"
IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
<primitives:ColorSlider x:Name="Component2Slider"
Grid.Column="2"
Grid.Row="3"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Component2"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentSliderVisible}" />
<!-- Component 3 RGB:Blue HSV:Value -->
<Border Grid.Column="0"
Grid.Row="4"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Height="{Binding ElementName=Component3NumericUpDown, Path=Bounds.Height}"
Background="{DynamicResource ThemeControlMidBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1,1,0,1"
CornerRadius="0,0,0,0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentTextInputVisible}">
<Panel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="B"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="V"
IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
</Panel>
</Border>
<NumericUpDown x:Name="Component3NumericUpDown"
Grid.Column="1"
Grid.Row="4"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,0,0,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=Component3Slider}"
Maximum="{Binding Maximum, ElementName=Component3Slider}"
Value="{Binding Value, ElementName=Component3Slider}"
IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
<primitives:ColorSlider x:Name="Component3Slider"
Grid.Column="2"
Grid.Row="4"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Component3"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{TemplateBinding IsComponentSliderVisible}" />
<!-- Alpha Component -->
<Border Grid.Column="0"
Grid.Row="5"
Width="{DynamicResource ColorViewComponentLabelWidth}"
Height="{Binding ElementName=AlphaComponentNumericUpDown, Path=Bounds.Height}"
Background="{DynamicResource ThemeControlMidBrush}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1,1,0,1"
CornerRadius="0,0,0,0"
VerticalAlignment="Center"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<TextBlock x:Name="AlphaComponentTextBlock"
Foreground="{DynamicResource ThemeForegroundBrush}"
FontWeight="SemiBold"
Text="A"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsComponentTextInputVisible" />
</MultiBinding>
</Border.IsVisible>
</Border>
<NumericUpDown x:Name="AlphaComponentNumericUpDown"
Grid.Column="1"
Grid.Row="5"
AllowSpin="True"
ShowButtonSpinner="False"
Height="32"
Width="{DynamicResource ColorViewComponentTextInputWidth}"
CornerRadius="0,0,0,0"
Margin="0,0,12,0"
VerticalAlignment="Center"
NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
Minimum="{Binding Minimum, ElementName=AlphaComponentSlider}"
Maximum="{Binding Maximum, ElementName=AlphaComponentSlider}"
Value="{Binding Value, ElementName=AlphaComponentSlider}"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<NumericUpDown.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsComponentTextInputVisible" />
</MultiBinding>
</NumericUpDown.IsVisible>
</NumericUpDown>
<primitives:ColorSlider x:Name="AlphaComponentSlider"
Grid.Column="2"
Grid.Row="5"
Orientation="Horizontal"
IsRoundingEnabled="True"
IsSnapToTickEnabled="True"
TickFrequency="1"
ColorComponent="Alpha"
ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsEnabled="{TemplateBinding IsAlphaEnabled}">
<primitives:ColorSlider.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsAlphaVisible" />
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="IsComponentSliderVisible" />
</MultiBinding>
</primitives:ColorSlider.IsVisible>
</primitives:ColorSlider>
</Grid>
</TabItem>
</TabControl>
<!-- Previewer -->
<!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
<primitives:ColorPreviewer Grid.Row="1"
HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
Margin="12,-5,12,7"
IsVisible="{TemplateBinding IsColorPreviewVisible}" />
</Grid>
</Flyout> </Flyout>
</DropDownButton.Flyout> </DropDownButton.Flyout>
</DropDownButton> </DropDownButton>

4
src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs

@ -28,8 +28,8 @@ namespace Avalonia.Controls
{ {
if (targetType != null && targetType.IsNullableType()) if (targetType != null && targetType.IsNullableType())
{ {
String strValue = value as String; var strValue = value as string;
if (strValue == String.Empty) if (string.IsNullOrEmpty(strValue))
{ {
return null; return null;
} }

4
src/Avalonia.Controls/AppBuilderBase.cs

@ -210,9 +210,9 @@ namespace Avalonia.Controls
{ {
var moduleInitializers = from assembly in AppDomain.CurrentDomain.GetAssemblies() var moduleInitializers = from assembly in AppDomain.CurrentDomain.GetAssemblies()
from attribute in assembly.GetCustomAttributes<ExportAvaloniaModuleAttribute>() from attribute in assembly.GetCustomAttributes<ExportAvaloniaModuleAttribute>()
where attribute.ForWindowingSubsystem == "" where string.IsNullOrEmpty(attribute.ForWindowingSubsystem)
|| attribute.ForWindowingSubsystem == WindowingSubsystemName || attribute.ForWindowingSubsystem == WindowingSubsystemName
where attribute.ForRenderingSubsystem == "" where string.IsNullOrEmpty(attribute.ForRenderingSubsystem)
|| attribute.ForRenderingSubsystem == RenderingSubsystemName || attribute.ForRenderingSubsystem == RenderingSubsystemName
group attribute by attribute.Name into exports group attribute by attribute.Name into exports
select (from export in exports select (from export in exports

41
src/Avalonia.Controls/Control.cs

@ -84,6 +84,13 @@ namespace Avalonia.Controls
nameof(Unloaded), nameof(Unloaded),
RoutingStrategies.Direct); 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> /// <summary>
/// Defines the <see cref="FlowDirection"/> property. /// Defines the <see cref="FlowDirection"/> property.
/// </summary> /// </summary>
@ -211,6 +218,15 @@ namespace Avalonia.Controls
remove => RemoveHandler(UnloadedEvent, value); 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; public new IControl? Parent => (IControl?)base.Parent;
/// <summary> /// <summary>
@ -530,14 +546,35 @@ namespace Avalonia.Controls
} }
} }
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(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(); InvalidateMirrorTransform();
foreach (var visual in VisualChildren) foreach (var visual in VisualChildren)
{ {
if (visual is Control child) if (visual is Control child)

2
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@ -99,7 +99,7 @@ namespace Avalonia.Controls
get => _clockIdentifier; get => _clockIdentifier;
set set
{ {
if (!(string.IsNullOrEmpty(value) || value == "" || value == "12HourClock" || value == "24HourClock")) if (!(string.IsNullOrEmpty(value) || value == "12HourClock" || value == "24HourClock"))
throw new ArgumentException("Invalid ClockIdentifier"); throw new ArgumentException("Invalid ClockIdentifier");
SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value); SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value);
SetGrid(); SetGrid();

2
src/Avalonia.Controls/DefinitionBase.cs

@ -366,7 +366,7 @@ namespace Avalonia.Controls
string id = (string)value; string id = (string)value;
if (id != string.Empty) if (!string.IsNullOrEmpty(id))
{ {
int i = -1; int i = -1;
while (++i < id.Length) while (++i < id.Length)

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

@ -12,7 +12,7 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection] [WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList<Inline> public class InlineCollection : AvaloniaList<Inline>
{ {
private ILogical? _parent; private IAvaloniaList<ILogical>? _logicalChildren;
private IInlineHost? _inlineHost; private IInlineHost? _inlineHost;
/// <summary> /// <summary>
@ -24,28 +24,30 @@ namespace Avalonia.Controls.Documents
this.ForEachItem( this.ForEachItem(
x => x =>
{ {
((ISetLogicalParent)x).SetParent(Parent);
x.InlineHost = InlineHost; x.InlineHost = InlineHost;
LogicalChildren?.Add(x);
Invalidate(); Invalidate();
}, },
x => x =>
{ {
((ISetLogicalParent)x).SetParent(null); LogicalChildren?.Remove(x);
x.InlineHost = InlineHost; x.InlineHost = InlineHost;
Invalidate(); Invalidate();
}, },
() => throw new NotSupportedException()); () => throw new NotSupportedException());
} }
internal ILogical? Parent internal IAvaloniaList<ILogical>? LogicalChildren
{ {
get => _parent; get => _logicalChildren;
set set
{ {
_parent = value; var oldValue = _logicalChildren;
_logicalChildren = value;
OnParentChanged(value); OnParentChanged(oldValue, value);
} }
} }
@ -70,6 +72,11 @@ namespace Avalonia.Controls.Documents
{ {
get get
{ {
if (Count == 0)
{
return null;
}
var builder = StringBuilderCache.Acquire(); var builder = StringBuilderCache.Acquire();
foreach (var inline in this) 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> /// <summary>
/// Add a text segment to the collection. /// Adds a text segment to the collection.
/// <remarks> /// <remarks>
/// For non complex content this appends the text to the end of currently held text. /// 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. /// For complex content this adds a <see cref="Run"/> to the collection.
/// </remarks> /// </remarks>
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text">The to be added text.</param>
public void Add(string text) public void Add(string text)
{ {
AddText(text); if (InlineHost is TextBlock textBlock && !textBlock.HasComplexContent)
}
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)
{ {
textBlock._text += text; textBlock._text += text;
} }
else 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) Add(new InlineUIContainer(control));
{
if (!textBlock.HasComplexContent && !string.IsNullOrEmpty(textBlock._text))
{
base.Add(new Run(textBlock._text));
textBlock._text = null;
}
}
} }
/// <summary> /// <summary>
@ -152,20 +148,21 @@ namespace Avalonia.Controls.Documents
Invalidated?.Invoke(this, EventArgs.Empty); Invalidated?.Invoke(this, EventArgs.Empty);
} }
private void OnParentChanged(ILogical? parent) private void OnParentChanged(IAvaloniaList<ILogical>? oldParent, IAvaloniaList<ILogical>? newParent)
{ {
foreach (var child in this) foreach (var child in this)
{ {
var oldParent = child.Parent; if (oldParent != newParent)
if (oldParent != parent)
{ {
if (oldParent != null) 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 Inlines = new InlineCollection
{ {
Parent = this LogicalChildren = LogicalChildren
}; };
} }
@ -78,14 +78,14 @@ namespace Avalonia.Controls.Documents
{ {
if (oldValue is not null) if (oldValue is not null)
{ {
oldValue.Parent = null; oldValue.LogicalChildren = null;
oldValue.InlineHost = null; oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate(); oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate();
} }
if (newValue is not null) if (newValue is not null)
{ {
newValue.Parent = this; newValue.LogicalChildren = LogicalChildren;
newValue.InlineHost = InlineHost; newValue.InlineHost = InlineHost;
newValue.Invalidated += (s, e) => InlineHost?.Invalidate(); newValue.Invalidated += (s, e) => InlineHost?.Invalidate();
} }

3
src/Avalonia.Controls/ListBox.cs

@ -139,7 +139,8 @@ namespace Avalonia.Controls
e.Source, e.Source,
true, true,
e.KeyModifiers.HasAllFlags(KeyModifiers.Shift), e.KeyModifiers.HasAllFlags(KeyModifiers.Shift),
e.KeyModifiers.HasAllFlags(KeyModifiers.Control)); e.KeyModifiers.HasAllFlags(KeyModifiers.Control),
fromFocus: true);
} }
} }

6
src/Avalonia.Controls/Platform/IScreenImpl.cs

@ -6,8 +6,14 @@ namespace Avalonia.Platform
[Unstable] [Unstable]
public interface IScreenImpl public interface IScreenImpl
{ {
/// <summary>
/// Gets the total number of screens available on the device.
/// </summary>
int ScreenCount { get; } int ScreenCount { get; }
/// <summary>
/// Gets the list of all screens available on the device.
/// </summary>
IReadOnlyList<Screen> AllScreens { get; } IReadOnlyList<Screen> AllScreens { get; }
Screen? ScreenFromWindow(IWindowBaseImpl window); Screen? ScreenFromWindow(IWindowBaseImpl window);

57
src/Avalonia.Controls/Platform/Screen.cs

@ -1,21 +1,64 @@
namespace Avalonia.Platform using System;
namespace Avalonia.Platform
{ {
/// <summary>
/// Represents a single display screen.
/// </summary>
public class Screen public class Screen
{ {
public double PixelDensity { get; } /// <summary>
/// Gets the scaling factor applied to the screen by the operating system.
/// </summary>
/// <remarks>
/// Multiply this value by 100 to get a percentage.
/// Both X and Y scaling factors are assumed uniform.
/// </remarks>
public double Scaling { get; }
/// <inheritdoc cref="Scaling"/>
[Obsolete("Use the Scaling property instead.")]
public double PixelDensity => Scaling;
/// <summary>
/// Gets the overall pixel-size of the screen.
/// </summary>
/// <remarks>
/// This generally is the raw pixel counts in both the X and Y direction.
/// </remarks>
public PixelRect Bounds { get; } public PixelRect Bounds { get; }
/// <summary>
/// Gets the actual working-area pixel-size of the screen.
/// </summary>
/// <remarks>
/// This area may be smaller than <see href="Bounds"/> to account for notches and
/// other block-out areas such as taskbars etc.
/// </remarks>
public PixelRect WorkingArea { get; } public PixelRect WorkingArea { get; }
public bool Primary { get; } /// <summary>
/// Gets a value indicating whether the screen is the primary one.
public Screen(double pixelDensity, PixelRect bounds, PixelRect workingArea, bool primary) /// </summary>
public bool IsPrimary { get; }
/// <inheritdoc cref="IsPrimary"/>
[Obsolete("Use the IsPrimary property instead.")]
public bool Primary => IsPrimary;
/// <summary>
/// Initializes a new instance of the <see cref="Screen"/> class.
/// </summary>
/// <param name="scaling">The scaling factor applied to the screen by the operating system.</param>
/// <param name="bounds">The overall pixel-size of the screen.</param>
/// <param name="workingArea">The actual working-area pixel-size of the screen.</param>
/// <param name="isPrimary">Whether the screen is the primary one.</param>
public Screen(double scaling, PixelRect bounds, PixelRect workingArea, bool isPrimary)
{ {
this.PixelDensity = pixelDensity; this.Scaling = scaling;
this.Bounds = bounds; this.Bounds = bounds;
this.WorkingArea = workingArea; this.WorkingArea = workingArea;
this.Primary = primary; this.IsPrimary = isPrimary;
} }
} }
} }

20
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -80,6 +80,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<double> LineHeightProperty = public static readonly StyledProperty<double> LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner<TextPresenter>(); TextBlock.LineHeightProperty.AddOwner<TextPresenter>();
/// <summary>
/// Defines the <see cref="LetterSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> LetterSpacingProperty =
TextBlock.LetterSpacingProperty.AddOwner<TextPresenter>();
/// <summary> /// <summary>
/// Defines the <see cref="Background"/> property. /// Defines the <see cref="Background"/> property.
/// </summary> /// </summary>
@ -212,6 +218,15 @@ namespace Avalonia.Controls.Presenters
set => SetValue(LineHeightProperty, value); set => SetValue(LineHeightProperty, value);
} }
/// <summary>
/// Gets or sets the letter spacing.
/// </summary>
public double LetterSpacing
{
get => GetValue(LetterSpacingProperty);
set => SetValue(LetterSpacingProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the text alignment. /// Gets or sets the text alignment.
/// </summary> /// </summary>
@ -333,7 +348,7 @@ namespace Avalonia.Controls.Presenters
var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment, var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment,
TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides, TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
flowDirection: FlowDirection, lineHeight: LineHeight); flowDirection: FlowDirection, lineHeight: LineHeight, letterSpacing: LetterSpacing);
return textLayout; return textLayout;
} }
@ -916,6 +931,9 @@ namespace Avalonia.Controls.Presenters
case nameof(TextAlignment): case nameof(TextAlignment):
case nameof(TextWrapping): case nameof(TextWrapping):
case nameof(LineHeight):
case nameof(LetterSpacing):
case nameof(SelectionStart): case nameof(SelectionStart):
case nameof(SelectionEnd): case nameof(SelectionEnd):
case nameof(SelectionForegroundBrush): case nameof(SelectionForegroundBrush):

45
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -586,6 +586,14 @@ namespace Avalonia.Controls.Primitives
Selection.SelectAll(); Selection.SelectAll();
e.Handled = true; e.Handled = true;
} }
else if (e.Key == Key.Space || e.Key == Key.Enter)
{
e.Handled = UpdateSelectionFromEventSource(
e.Source,
true,
e.KeyModifiers.HasFlag(KeyModifiers.Shift),
e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
} }
} }
@ -662,12 +670,14 @@ namespace Avalonia.Controls.Primitives
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param> /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param> /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param> /// <param name="rightButton">Whether the event is a right-click.</param>
/// <param name="fromFocus">Wheter the event is a focus event</param>
protected void UpdateSelection( protected void UpdateSelection(
int index, int index,
bool select = true, bool select = true,
bool rangeModifier = false, bool rangeModifier = false,
bool toggleModifier = false, bool toggleModifier = false,
bool rightButton = false) bool rightButton = false,
bool fromFocus = false)
{ {
if (index < 0 || index >= ItemCount) if (index < 0 || index >= ItemCount)
{ {
@ -696,22 +706,25 @@ namespace Avalonia.Controls.Primitives
Selection.Clear(); Selection.Clear();
Selection.SelectRange(Selection.AnchorIndex, index); Selection.SelectRange(Selection.AnchorIndex, index);
} }
else if (multi && toggle) else if (!fromFocus && toggle)
{ {
if (Selection.IsSelected(index) == true) if (multi)
{ {
Selection.Deselect(index); if (Selection.IsSelected(index) == true)
{
Selection.Deselect(index);
}
else
{
Selection.Select(index);
}
} }
else else
{ {
Selection.Select(index); SelectedIndex = (SelectedIndex == index) ? -1 : index;
} }
} }
else if (toggle) else if (!toggle)
{
SelectedIndex = (SelectedIndex == index) ? -1 : index;
}
else
{ {
using var operation = Selection.BatchUpdate(); using var operation = Selection.BatchUpdate();
Selection.Clear(); Selection.Clear();
@ -735,18 +748,20 @@ namespace Avalonia.Controls.Primitives
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param> /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param> /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param> /// <param name="rightButton">Whether the event is a right-click.</param>
/// <param name="fromFocus">Wheter the event is a focus event</param>
protected void UpdateSelection( protected void UpdateSelection(
IControl container, IControl container,
bool select = true, bool select = true,
bool rangeModifier = false, bool rangeModifier = false,
bool toggleModifier = false, bool toggleModifier = false,
bool rightButton = false) bool rightButton = false,
bool fromFocus = false)
{ {
var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1; var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1;
if (index != -1) if (index != -1)
{ {
UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton); UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton, fromFocus);
} }
} }
@ -759,6 +774,7 @@ namespace Avalonia.Controls.Primitives
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param> /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param> /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param> /// <param name="rightButton">Whether the event is a right-click.</param>
/// <param name="fromFocus">Wheter the event is a focus event</param>
/// <returns> /// <returns>
/// True if the event originated from a container that belongs to the control; otherwise /// True if the event originated from a container that belongs to the control; otherwise
/// false. /// false.
@ -768,13 +784,14 @@ namespace Avalonia.Controls.Primitives
bool select = true, bool select = true,
bool rangeModifier = false, bool rangeModifier = false,
bool toggleModifier = false, bool toggleModifier = false,
bool rightButton = false) bool rightButton = false,
bool fromFocus = false)
{ {
var container = GetContainerFromEventSource(eventSource); var container = GetContainerFromEventSource(eventSource);
if (container != null) if (container != null)
{ {
UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton); UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton, fromFocus);
return true; return true;
} }

19
src/Avalonia.Controls/ProgressBar.cs

@ -110,7 +110,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty<string> ProgressTextFormatProperty = public static readonly StyledProperty<string> ProgressTextFormatProperty =
AvaloniaProperty.Register<ProgressBar, string>(nameof(ProgressTextFormat), "{1:0}%"); AvaloniaProperty.Register<ProgressBar, string>(nameof(ProgressTextFormat), "{1:0}%");
public static readonly StyledProperty<Orientation> OrientationProperty = public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal); AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal);
@ -136,7 +136,7 @@ namespace Avalonia.Controls
get { return _percentage; } get { return _percentage; }
private set { SetAndRaise(PercentageProperty, ref _percentage, value); } private set { SetAndRaise(PercentageProperty, ref _percentage, value); }
} }
public double IndeterminateStartingOffset public double IndeterminateStartingOffset
{ {
get => _indeterminateStartingOffset; get => _indeterminateStartingOffset;
@ -156,6 +156,7 @@ namespace Avalonia.Controls
MinimumProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e)); MinimumProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
MaximumProperty.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)); IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
OrientationProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
} }
public ProgressBar() public ProgressBar()
@ -216,13 +217,13 @@ namespace Avalonia.Controls
{ {
// dispose any previous track size listener // dispose any previous track size listener
_trackSizeChangedListener?.Dispose(); _trackSizeChangedListener?.Dispose();
_indicator = e.NameScope.Get<Border>("PART_Indicator"); _indicator = e.NameScope.Get<Border>("PART_Indicator");
// listen to size changes of the indicators track (parent) and update the indicator there. // listen to size changes of the indicators track (parent) and update the indicator there.
_trackSizeChangedListener = _indicator.Parent?.GetPropertyChangedObservable(BoundsProperty) _trackSizeChangedListener = _indicator.Parent?.GetPropertyChangedObservable(BoundsProperty)
.Subscribe(_ => UpdateIndicator()); .Subscribe(_ => UpdateIndicator());
UpdateIndicator(); UpdateIndicator();
} }
@ -230,7 +231,7 @@ namespace Avalonia.Controls
{ {
// Gets the size of the parent indicator container // Gets the size of the parent indicator container
var barSize = _indicator?.Parent?.Bounds.Size ?? Bounds.Size; var barSize = _indicator?.Parent?.Bounds.Size ?? Bounds.Size;
if (_indicator != null) if (_indicator != null)
{ {
if (IsIndeterminate) if (IsIndeterminate)
@ -268,10 +269,18 @@ namespace Avalonia.Controls
{ {
double percent = Maximum == Minimum ? 1.0 : (Value - Minimum) / (Maximum - Minimum); 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) if (Orientation == Orientation.Horizontal)
{
_indicator.Width = barSize.Width * percent; _indicator.Width = barSize.Width * percent;
_indicator.Height = double.NaN;
}
else else
{
_indicator.Width = double.NaN;
_indicator.Height = barSize.Height * percent; _indicator.Height = barSize.Height * percent;
}
Percentage = percent * 100; Percentage = percent * 100;
} }

23
src/Avalonia.Controls/Screens.cs

@ -8,14 +8,31 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary>
/// Represents all screens available on a device.
/// </summary>
public class Screens public class Screens
{ {
private readonly IScreenImpl _iScreenImpl; private readonly IScreenImpl _iScreenImpl;
/// <summary>
/// Gets the total number of screens available on the device.
/// </summary>
public int ScreenCount => _iScreenImpl?.ScreenCount ?? 0; public int ScreenCount => _iScreenImpl?.ScreenCount ?? 0;
/// <summary>
/// Gets the list of all screens available on the device.
/// </summary>
public IReadOnlyList<Screen> All => _iScreenImpl?.AllScreens ?? Array.Empty<Screen>(); public IReadOnlyList<Screen> All => _iScreenImpl?.AllScreens ?? Array.Empty<Screen>();
public Screen? Primary => All.FirstOrDefault(x => x.Primary);
/// <summary>
/// Gets the primary screen on the device.
/// </summary>
public Screen? Primary => All.FirstOrDefault(x => x.IsPrimary);
/// <summary>
/// Initializes a new instance of the <see cref="Screens"/> class.
/// </summary>
public Screens(IScreenImpl iScreenImpl) public Screens(IScreenImpl iScreenImpl)
{ {
_iScreenImpl = iScreenImpl; _iScreenImpl = iScreenImpl;
@ -25,14 +42,14 @@ namespace Avalonia.Controls
{ {
return _iScreenImpl.ScreenFromRect(bounds); return _iScreenImpl.ScreenFromRect(bounds);
} }
public Screen? ScreenFromWindow(IWindowBaseImpl window) public Screen? ScreenFromWindow(IWindowBaseImpl window)
{ {
return _iScreenImpl.ScreenFromWindow(window); return _iScreenImpl.ScreenFromWindow(window);
} }
public Screen? ScreenFromPoint(PixelPoint point) public Screen? ScreenFromPoint(PixelPoint point)
{ {
return _iScreenImpl.ScreenFromPoint(point); return _iScreenImpl.ScreenFromPoint(point);
} }

374
src/Avalonia.Controls/RichTextBlock.cs → src/Avalonia.Controls/SelectableTextBlock.cs

@ -8,7 +8,6 @@ using Avalonia.Input.Platform;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -16,67 +15,53 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control that displays a block of formatted text. /// A control that displays a block of formatted text.
/// </summary> /// </summary>
public class RichTextBlock : TextBlock, IInlineHost public class SelectableTextBlock : TextBlock, IInlineHost
{ {
public static readonly StyledProperty<bool> IsTextSelectionEnabledProperty = public static readonly DirectProperty<SelectableTextBlock, int> SelectionStartProperty =
AvaloniaProperty.Register<RichTextBlock, bool>(nameof(IsTextSelectionEnabled), false); AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
public static readonly DirectProperty<RichTextBlock, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
nameof(SelectionStart), nameof(SelectionStart),
o => o.SelectionStart, o => o.SelectionStart,
(o, v) => o.SelectionStart = v); (o, v) => o.SelectionStart = v);
public static readonly DirectProperty<RichTextBlock, int> SelectionEndProperty = public static readonly DirectProperty<SelectableTextBlock, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>( AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
nameof(SelectionEnd), nameof(SelectionEnd),
o => o.SelectionEnd, o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v); (o, v) => o.SelectionEnd = v);
public static readonly DirectProperty<RichTextBlock, string> SelectedTextProperty = public static readonly DirectProperty<SelectableTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, string>( AvaloniaProperty.RegisterDirect<SelectableTextBlock, string>(
nameof(SelectedText), nameof(SelectedText),
o => o.SelectedText); o => o.SelectedText);
public static readonly StyledProperty<IBrush?> SelectionBrushProperty = 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 = public static readonly DirectProperty<SelectableTextBlock, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>( AvaloniaProperty.RegisterDirect<SelectableTextBlock, bool>(
nameof(CanCopy), nameof(CanCopy),
o => o.CanCopy); o => o.CanCopy);
public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent = public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
RoutedEvent.Register<RichTextBlock, RoutedEventArgs>( RoutedEvent.Register<SelectableTextBlock, RoutedEventArgs>(
nameof(CopyingToClipboard), RoutingStrategies.Bubble); nameof(CopyingToClipboard), RoutingStrategies.Bubble);
private bool _canCopy; private bool _canCopy;
private int _selectionStart; private int _selectionStart;
private int _selectionEnd; private int _selectionEnd;
private int _wordSelectionStart = -1; private int _wordSelectionStart = -1;
private IReadOnlyList<TextRun>? _textRuns;
static RichTextBlock() static SelectableTextBlock()
{ {
FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true); FocusableProperty.OverrideDefaultValue(typeof(SelectableTextBlock), true);
AffectsRender<SelectableTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty);
AffectsRender<RichTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty);
} }
public RichTextBlock() public event EventHandler<RoutedEventArgs>? CopyingToClipboard
{ {
Inlines = new InlineCollection add => AddHandler(CopyingToClipboardEvent, value);
{ remove => RemoveHandler(CopyingToClipboardEvent, value);
Parent = this,
InlineHost = this
};
} }
/// <summary> /// <summary>
@ -99,6 +84,8 @@ namespace Avalonia.Controls
if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value)) if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
{ {
RaisePropertyChanged(SelectedTextProperty, "", ""); RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
} }
} }
} }
@ -114,6 +101,8 @@ namespace Avalonia.Controls
if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value)) if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
{ {
RaisePropertyChanged(SelectedTextProperty, "", ""); RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
} }
} }
} }
@ -126,25 +115,6 @@ namespace Avalonia.Controls
get => GetSelection(); 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> /// <summary>
/// Property for determining if the Copy command can be executed. /// Property for determining if the Copy command can be executed.
/// </summary> /// </summary>
@ -154,20 +124,12 @@ namespace Avalonia.Controls
private set => SetAndRaise(CanCopyProperty, ref _canCopy, value); 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> /// <summary>
/// Copies the current selection to the Clipboard. /// Copies the current selection to the Clipboard.
/// </summary> /// </summary>
public async void Copy() public async void Copy()
{ {
if (_canCopy || !IsTextSelectionEnabled) if (!_canCopy)
{ {
return; return;
} }
@ -188,45 +150,13 @@ namespace Avalonia.Controls
await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard))) await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
.SetTextAsync(text); .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> /// <summary>
/// Select all text in the TextBox /// Select all text in the TextBox
/// </summary> /// </summary>
public void SelectAll() public void SelectAll()
{ {
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text; var text = Text;
SelectionStart = 0; SelectionStart = 0;
@ -238,94 +168,52 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public void ClearSelection() public void ClearSelection()
{ {
if (!IsTextSelectionEnabled)
{
return;
}
SelectionEnd = SelectionStart; SelectionEnd = SelectionStart;
} }
protected void AddText(string? text) protected override void OnGotFocus(GotFocusEventArgs e)
{ {
if (string.IsNullOrEmpty(text)) base.OnGotFocus(e);
{
return;
}
if (!HasComplexContent && string.IsNullOrEmpty(_text))
{
_text = text;
}
else
{
if (!string.IsNullOrEmpty(_text))
{
Inlines?.Add(_text);
_text = null;
}
Inlines?.Add(text);
}
}
protected override string? GetText() UpdateCommandStates();
{
return _text ?? Inlines?.Text;
} }
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> protected override void RenderTextLayout(DrawingContext context, Point origin)
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected override TextLayout CreateTextLayout(string? text)
{ {
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); var selectionStart = SelectionStart;
var defaultProperties = new GenericTextRunProperties( var selectionEnd = SelectionEnd;
typeface, var selectionBrush = SelectionBrush;
FontSize,
TextDecorations,
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
ITextSource textSource;
if (_textRuns != null) if (selectionStart != selectionEnd && selectionBrush != null)
{ {
textSource = new InlinesTextSource(_textRuns); var start = Math.Min(selectionStart, selectionEnd);
} var length = Math.Max(selectionStart, selectionEnd) - start;
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
return new TextLayout( var rects = TextLayout.HitTestTextRange(start, length);
textSource,
paragraphProperties,
TextTrimming,
_constraint.Width,
_constraint.Height,
maxLines: MaxLines,
lineHeight: LineHeight);
}
protected override void OnLostFocus(RoutedEventArgs e) using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
{ {
base.OnLostFocus(e); 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) protected override void OnKeyDown(KeyEventArgs e)
@ -352,11 +240,6 @@ namespace Avalonia.Controls
{ {
base.OnPointerPressed(e); base.OnPointerPressed(e);
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text; var text = Text;
var clickInfo = e.GetCurrentPoint(this); var clickInfo = e.GetCurrentPoint(this);
@ -435,11 +318,6 @@ namespace Avalonia.Controls
{ {
base.OnPointerMoved(e); base.OnPointerMoved(e);
if (!IsTextSelectionEnabled)
{
return;
}
// selection should not change during pointer move if the user right clicks // selection should not change during pointer move if the user right clicks
if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
@ -486,11 +364,6 @@ namespace Avalonia.Controls
{ {
base.OnPointerReleased(e); base.OnPointerReleased(e);
if (!IsTextSelectionEnabled)
{
return;
}
if (e.Pointer.Captured != this) if (e.Pointer.Captured != this)
{ {
return; return;
@ -521,100 +394,15 @@ namespace Avalonia.Controls
e.Pointer.Capture(null); e.Pointer.Capture(null);
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) private void UpdateCommandStates()
{
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)
{ {
if(_textRuns != null) var text = GetSelection();
{
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;
}
}
currentY += textLine.Height; CanCopy = !string.IsNullOrEmpty(text);
}
}
return base.ArrangeOverride(finalSize);
} }
private string GetSelection() private string GetSelection()
{ {
if (!IsTextSelectionEnabled)
{
return "";
}
var text = GetText(); var text = GetText();
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(text))
@ -638,59 +426,5 @@ namespace Avalonia.Controls
return selectedText; 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;
using System.Collections.Generic;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Layout; using Avalonia.Layout;
@ -12,7 +13,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control that displays a block of text. /// A control that displays a block of text.
/// </summary> /// </summary>
public class TextBlock : Control, IAddChild<string> public class TextBlock : Control, IInlineHost
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Background"/> property. /// Defines the <see cref="Background"/> property.
@ -81,6 +82,15 @@ namespace Avalonia.Controls
validate: IsValidLineHeight, validate: IsValidLineHeight,
inherits: true); 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> /// <summary>
/// Defines the <see cref="MaxLines"/> property. /// Defines the <see cref="MaxLines"/> property.
/// </summary> /// </summary>
@ -96,15 +106,15 @@ namespace Avalonia.Controls
public static readonly DirectProperty<TextBlock, string?> TextProperty = public static readonly DirectProperty<TextBlock, string?> TextProperty =
AvaloniaProperty.RegisterDirect<TextBlock, string?>( AvaloniaProperty.RegisterDirect<TextBlock, string?>(
nameof(Text), nameof(Text),
o => o.Text, o => o.GetText(),
(o, v) => o.Text = v); (o, v) => o.SetText(v));
/// <summary> /// <summary>
/// Defines the <see cref="TextAlignment"/> property. /// Defines the <see cref="TextAlignment"/> property.
/// </summary> /// </summary>
public static readonly AttachedProperty<TextAlignment> TextAlignmentProperty = public static readonly AttachedProperty<TextAlignment> TextAlignmentProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextAlignment>( AvaloniaProperty.RegisterAttached<TextBlock, Control, TextAlignment>(
nameof(TextAlignment), nameof(TextAlignment),
defaultValue: TextAlignment.Start, defaultValue: TextAlignment.Start,
inherits: true); inherits: true);
@ -112,14 +122,14 @@ namespace Avalonia.Controls
/// Defines the <see cref="TextWrapping"/> property. /// Defines the <see cref="TextWrapping"/> property.
/// </summary> /// </summary>
public static readonly AttachedProperty<TextWrapping> TextWrappingProperty = public static readonly AttachedProperty<TextWrapping> TextWrappingProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextWrapping>(nameof(TextWrapping), AvaloniaProperty.RegisterAttached<TextBlock, Control, TextWrapping>(nameof(TextWrapping),
inherits: true); inherits: true);
/// <summary> /// <summary>
/// Defines the <see cref="TextTrimming"/> property. /// Defines the <see cref="TextTrimming"/> property.
/// </summary> /// </summary>
public static readonly AttachedProperty<TextTrimming> TextTrimmingProperty = public static readonly AttachedProperty<TextTrimming> TextTrimmingProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextTrimming>(nameof(TextTrimming), AvaloniaProperty.RegisterAttached<TextBlock, Control, TextTrimming>(nameof(TextTrimming),
defaultValue: TextTrimming.None, defaultValue: TextTrimming.None,
inherits: true); inherits: true);
@ -129,9 +139,17 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty = public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations)); 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; internal string? _text;
protected TextLayout? _textLayout; protected TextLayout? _textLayout;
protected Size _constraint; protected Size _constraint;
private IReadOnlyList<TextRun>? _textRuns;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TextBlock"/> class. /// Initializes static members of the <see cref="TextBlock"/> class.
@ -139,10 +157,19 @@ namespace Avalonia.Controls
static TextBlock() static TextBlock()
{ {
ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true); ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty); AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty);
} }
public TextBlock()
{
Inlines = new InlineCollection
{
LogicalChildren = LogicalChildren,
InlineHost = this
};
}
/// <summary> /// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text. /// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
@ -244,6 +271,15 @@ namespace Avalonia.Controls
set => SetValue(LineHeightProperty, value); set => SetValue(LineHeightProperty, value);
} }
/// <summary>
/// Gets or sets the letter spacing.
/// </summary>
public double LetterSpacing
{
get => GetValue(LetterSpacingProperty);
set => SetValue(LetterSpacingProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the maximum number of text lines. /// Gets or sets the maximum number of text lines.
/// </summary> /// </summary>
@ -288,9 +324,21 @@ namespace Avalonia.Controls
get => GetValue(TextDecorationsProperty); get => GetValue(TextDecorationsProperty);
set => SetValue(TextDecorationsProperty, value); 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; protected override bool BypassFlowDirectionPolicies => true;
internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
/// <summary> /// <summary>
/// The BaselineOffset property provides an adjustment to baseline offset /// The BaselineOffset property provides an adjustment to baseline offset
/// </summary> /// </summary>
@ -445,6 +493,35 @@ namespace Avalonia.Controls
control.SetValue(LineHeightProperty, height); 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> /// <summary>
/// Reads the attached property from the given element /// Reads the attached property from the given element
/// </summary> /// </summary>
@ -513,19 +590,19 @@ namespace Avalonia.Controls
TextLayout.Draw(context, origin); TextLayout.Draw(context, origin);
} }
void IAddChild<string>.AddChild(string text)
{
_text = text;
}
protected virtual string? GetText() protected virtual string? GetText()
{ {
return _text; return _text ?? Inlines?.Text;
} }
protected virtual void SetText(string? text) protected virtual void SetText(string? text)
{ {
SetAndRaise(TextProperty, ref _text, text); if (HasComplexContent)
{
Inlines?.Clear();
}
SetAndRaise(TextProperty, ref _text, text);
} }
/// <summary> /// <summary>
@ -534,17 +611,30 @@ namespace Avalonia.Controls
/// <returns>A <see cref="TextLayout"/> object.</returns> /// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual TextLayout CreateTextLayout(string? text) protected virtual TextLayout CreateTextLayout(string? text)
{ {
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var defaultProperties = new GenericTextRunProperties( var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), typeface,
FontSize, FontSize,
TextDecorations, TextDecorations,
Foreground); Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, 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( return new TextLayout(
new SimpleTextSource((text ?? "").AsMemory(), defaultProperties), textSource,
paragraphProperties, paragraphProperties,
TextTrimming, TextTrimming,
_constraint.Width, _constraint.Width,
@ -560,6 +650,8 @@ namespace Avalonia.Controls
{ {
_textLayout = null; _textLayout = null;
InvalidateVisual();
InvalidateMeasure(); InvalidateMeasure();
} }
@ -573,7 +665,46 @@ namespace Avalonia.Controls
_textLayout = null; _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); var measuredSize = TextLayout.Bounds.Size.Inflate(padding);
@ -584,16 +715,11 @@ namespace Avalonia.Controls
{ {
var textWidth = Math.Ceiling(TextLayout.Bounds.Width); var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
if(finalSize.Width < textWidth) if (finalSize.Width < textWidth)
{ {
finalSize = finalSize.WithWidth(textWidth); finalSize = finalSize.WithWidth(textWidth);
} }
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
{
return finalSize;
}
var scale = LayoutHelper.GetLayoutScale(this); var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
@ -602,6 +728,32 @@ namespace Avalonia.Controls
_textLayout = null; _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; return finalSize;
} }
@ -610,42 +762,71 @@ namespace Avalonia.Controls
return new TextBlockAutomationPeer(this); 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) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
switch (change.Property.Name) switch (change.Property.Name)
{ {
case nameof (FontSize): case nameof(FontSize):
case nameof (FontWeight): case nameof(FontWeight):
case nameof (FontStyle): case nameof(FontStyle):
case nameof (FontFamily): case nameof(FontFamily):
case nameof (FontStretch): case nameof(FontStretch):
case nameof (TextWrapping): case nameof(TextWrapping):
case nameof (TextTrimming): case nameof(TextTrimming):
case nameof (TextAlignment): case nameof(TextAlignment):
case nameof (FlowDirection): case nameof(FlowDirection):
case nameof (Padding): case nameof (Padding):
case nameof (LineHeight): case nameof (LineHeight):
case nameof (LetterSpacing):
case nameof (MaxLines): case nameof (MaxLines):
case nameof (Text): case nameof(Text):
case nameof (TextDecorations): case nameof(TextDecorations):
case nameof (Foreground): case nameof(Foreground):
{ {
InvalidateTextLayout(); InvalidateTextLayout();
break; 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 protected readonly struct SimpleTextSource : ITextSource
{ {
private readonly ReadOnlySlice<char> _text; private readonly ReadOnlySlice<char> _text;
@ -674,5 +855,46 @@ namespace Avalonia.Controls
return new TextCharacters(runText, _defaultProperties); 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;
}
}
} }
} }

12
src/Avalonia.Controls/TextBox.cs

@ -114,6 +114,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> LineHeightProperty = public static readonly StyledProperty<double> LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner<TextBox>(); 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 = public static readonly StyledProperty<string?> WatermarkProperty =
AvaloniaProperty.Register<TextBox, string?>(nameof(Watermark)); AvaloniaProperty.Register<TextBox, string?>(nameof(Watermark));
@ -378,6 +384,12 @@ namespace Avalonia.Controls
set => SetValue(MaxLinesProperty, value); set => SetValue(MaxLinesProperty, value);
} }
public double LetterSpacing
{
get => GetValue(LetterSpacingProperty);
set => SetValue(LetterSpacingProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the line height. /// Gets or sets the line height.
/// </summary> /// </summary>

30
src/Avalonia.Controls/TreeViewItem.cs

@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Threading;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -45,6 +46,8 @@ namespace Avalonia.Controls
private IControl? _header; private IControl? _header;
private bool _isExpanded; private bool _isExpanded;
private int _level; private int _level;
private bool _templateApplied;
private bool _deferredBringIntoViewFlag;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TreeViewItem"/> class. /// Initializes static members of the <see cref="TreeViewItem"/> class.
@ -136,15 +139,24 @@ namespace Avalonia.Controls
protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e) protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)
{ {
if (e.TargetObject == this && _header != null) if (e.TargetObject == this)
{ {
var m = _header.TransformToVisual(this); if (!_templateApplied)
{
_deferredBringIntoViewFlag = true;
return;
}
if (m.HasValue) if (_header != null)
{ {
var bounds = new Rect(_header.Bounds.Size); var m = _header.TransformToVisual(this);
var rect = bounds.TransformToAABB(m.Value);
e.TargetRect = rect; if (m.HasValue)
{
var bounds = new Rect(_header.Bounds.Size);
var rect = bounds.TransformToAABB(m.Value);
e.TargetRect = rect;
}
} }
} }
} }
@ -187,6 +199,12 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
_header = e.NameScope.Find<IControl>("PART_Header"); _header = e.NameScope.Find<IControl>("PART_Header");
_templateApplied = true;
if (_deferredBringIntoViewFlag)
{
_deferredBringIntoViewFlag = false;
Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll
}
} }
private static int CalculateDistanceFromLogicalParent<T>(ILogical? logical, int @default = -1) where T : class private static int CalculateDistanceFromLogicalParent<T>(ILogical? logical, int @default = -1) where T : class

1
src/Avalonia.Controls/Viewbox.cs

@ -42,6 +42,7 @@ namespace Avalonia.Controls
// can be applied independently of the Viewbox and Child transforms. // can be applied independently of the Viewbox and Child transforms.
_containerVisual = new ViewboxContainer(); _containerVisual = new ViewboxContainer();
_containerVisual.RenderTransformOrigin = RelativePoint.TopLeft; _containerVisual.RenderTransformOrigin = RelativePoint.TopLeft;
((ISetLogicalParent)_containerVisual).SetParent(this);
VisualChildren.Add(_containerVisual); VisualChildren.Add(_containerVisual);
} }

2
src/Avalonia.DesignerSupport/Remote/HtmlTransport/SimpleWebSocketHttpServer.cs

@ -72,7 +72,7 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport
while (true) while (true)
{ {
line = await ReadLineAsync(); line = await ReadLineAsync();
if (line == "") if (string.IsNullOrEmpty(line))
break; break;
sp = line.Split(new[] {':'}, 2); sp = line.Split(new[] {':'}, 2);
headers[sp[0]] = sp[1].TrimStart(); headers[sp[0]] = sp[1].TrimStart();

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

Loading…
Cancel
Save