Browse Source

Merge remote-tracking branch 'origin/master' into android-auto-detect-night-mode

pull/16340/head
Emmanuel Hansen 6 months ago
parent
commit
cf64f727d8
  1. 1
      Avalonia.Desktop.slnf
  2. 23
      Avalonia.sln
  3. 58
      api/Avalonia.nupkg.xml
  4. 4
      azure-pipelines.yml
  5. 14
      build/AnalyzerProject.targets
  6. 1
      build/TargetFrameworks.props
  7. 25
      docs/build.md
  8. 1
      docs/index.md
  9. 56
      docs/nuget.md
  10. 8
      docs/release.md
  11. 8
      native/Avalonia.Native/src/OSX/WindowImpl.h
  12. 27
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  13. 19
      samples/ControlCatalog.iOS/ControlCatalog.MacCatalyst.csproj
  14. 9
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  15. 21
      samples/ControlCatalog.iOS/ControlCatalog.tvOS.csproj
  16. 42
      samples/ControlCatalog.iOS/Info.Catalyst.plist
  17. 3
      samples/ControlCatalog.iOS/Info.iOS.plist
  18. 40
      samples/ControlCatalog.iOS/Info.tvOS.plist
  19. 3
      samples/ControlCatalog/MainWindow.xaml
  20. 5
      samples/ControlCatalog/Pages/CalendarPage.xaml
  21. 14
      samples/ControlCatalog/Pages/ComboBoxPage.xaml
  22. 3
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  23. 49
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  24. 14
      samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs
  25. 64
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  26. 2
      samples/Generators.Sandbox/Controls/SignUpView.xaml
  27. 6
      samples/IntegrationTestApp/Pages/WindowPage.axaml
  28. 5
      samples/IntegrationTestApp/Pages/WindowPage.axaml.cs
  29. 5
      samples/TextTestApp/App.axaml
  30. 21
      samples/TextTestApp/App.axaml.cs
  31. 24
      samples/TextTestApp/GridRow.cs
  32. 705
      samples/TextTestApp/InteractiveLineControl.cs
  33. 105
      samples/TextTestApp/MainWindow.axaml
  34. 340
      samples/TextTestApp/MainWindow.axaml.cs
  35. 25
      samples/TextTestApp/Program.cs
  36. 90
      samples/TextTestApp/SelectionAdorner.cs
  37. 23
      samples/TextTestApp/TextTestApp.csproj
  38. 28
      samples/TextTestApp/app.manifest
  39. 5
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  40. 67
      src/Android/Avalonia.Android/AvaloniaView.Input.cs
  41. 21
      src/Android/Avalonia.Android/AvaloniaView.cs
  42. 48
      src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs
  43. 3
      src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs
  44. 59
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  45. 2
      src/Avalonia.Base/Input/InputElement.cs
  46. 4
      src/Avalonia.Base/Media/CharacterHit.cs
  47. 85
      src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
  48. 29
      src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
  49. 66
      src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
  50. 122
      src/Avalonia.Base/Threading/DispatcherPriorityAwaitable.cs
  51. 111
      src/Avalonia.Base/Utilities/WeakEvent.cs
  52. 23
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs
  53. 5
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs
  54. 135
      src/Avalonia.Controls/Calendar/Calendar.cs
  55. 9
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  56. 55
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  57. 166
      src/Avalonia.Controls/ComboBox.cs
  58. 357
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
  59. 382
      src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
  60. 86
      src/Avalonia.Controls/GridSplitter.cs
  61. 11
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  62. 32
      src/Avalonia.Controls/SplitView/SplitView.cs
  63. 10
      src/Avalonia.Controls/Utils/BindingEvaluator.cs
  64. 12
      src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs
  65. 209
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  66. 70
      src/Avalonia.Controls/Window.cs
  67. 8
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  68. 8
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  69. 25
      src/Avalonia.Native/WindowImpl.cs
  70. 2
      src/Avalonia.Native/avn.idl
  71. 9
      src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml
  72. 1
      src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml
  73. 2
      src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml
  74. 56
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  75. 23
      src/Avalonia.Themes.Fluent/Controls/GroupBox.xaml
  76. 1
      src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml
  77. 37
      src/Avalonia.Themes.Simple/Controls/ComboBox.xaml
  78. 23
      src/Avalonia.Themes.Simple/Controls/GroupBox.xaml
  79. 42
      src/Avalonia.X11/X11Window.cs
  80. 8
      src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
  81. 2
      src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs
  82. 15
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  83. 2
      src/Windows/Avalonia.Win32/PopupImpl.cs
  84. 7
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  85. 2
      src/Windows/Avalonia.Win32/Win32Platform.cs
  86. 36
      src/Windows/Avalonia.Win32/WindowImpl.cs
  87. 3
      src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
  88. 70
      src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs
  89. 2
      src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs
  90. 13
      src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj
  91. 8
      src/tools/Avalonia.Generators/Avalonia.Generators.csproj
  92. 3
      src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs
  93. 4
      src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs
  94. 12
      src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs
  95. 41
      src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs
  96. 58
      src/tools/Avalonia.Generators/Common/EquatableList.cs
  97. 7
      src/tools/Avalonia.Generators/Common/GlobPattern.cs
  98. 22
      src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs
  99. 20
      src/tools/Avalonia.Generators/Common/ResolverExtensions.cs
  100. 65
      src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs

1
Avalonia.Desktop.slnf

@ -8,6 +8,7 @@
"samples\\ControlCatalog\\ControlCatalog.csproj",
"samples\\GpuInterop\\GpuInterop.csproj",
"samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
"samples\\TextTestApp\\TextTestApp.csproj",
"samples\\MiniMvvm\\MiniMvvm.csproj",
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"samples\\RenderDemo\\RenderDemo.csproj",

23
Avalonia.sln

@ -1,3 +1,4 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
@ -120,6 +121,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
build\WarnAsErrors.props = build\WarnAsErrors.props
build\XUnit.props = build\XUnit.props
build\AnalyzerProject.targets = build\AnalyzerProject.targets
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}"
@ -190,6 +192,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvv
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestApp", "samples\IntegrationTestApp\IntegrationTestApp.csproj", "{676D6BFD-029D-4E43-BFC7-3892265CE251}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextTestApp", "samples\TextTestApp\TextTestApp.csproj", "{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.IntegrationTests.Appium", "tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Browser", "Browser", "{86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}"
@ -299,6 +303,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32.Automation",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XEmbedSample", "samples\XEmbedSample\XEmbedSample.csproj", "{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.MacCatalyst", "samples\ControlCatalog.iOS\ControlCatalog.MacCatalyst.csproj", "{DE3C28DD-B602-4750-831D-345102A54CA0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.tvOS", "samples\ControlCatalog.iOS\ControlCatalog.tvOS.csproj", "{14342787-B4EF-4076-8C91-BA6C523DE8DF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -533,6 +541,10 @@ Global
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|Any CPU.Build.0 = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.ActiveCfg = Release|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.Build.0 = Release|Any CPU
{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Release|Any CPU.Build.0 = Release|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -699,6 +711,14 @@ Global
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Release|Any CPU.Build.0 = Release|Any CPU
{DE3C28DD-B602-4750-831D-345102A54CA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DE3C28DD-B602-4750-831D-345102A54CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE3C28DD-B602-4750-831D-345102A54CA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE3C28DD-B602-4750-831D-345102A54CA0}.Release|Any CPU.Build.0 = Release|Any CPU
{14342787-B4EF-4076-8C91-BA6C523DE8DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{14342787-B4EF-4076-8C91-BA6C523DE8DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{14342787-B4EF-4076-8C91-BA6C523DE8DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{14342787-B4EF-4076-8C91-BA6C523DE8DF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -748,6 +768,7 @@ Global
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{676D6BFD-029D-4E43-BFC7-3892265CE251} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098}
@ -787,6 +808,8 @@ Global
{9AE1B827-21AC-4063-AB22-C8804B7F931E} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{0097673D-DBCE-476E-82FE-E78A56E58AA2} = {B39A8919-9F95-48FE-AD7B-76E08B509888}
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{DE3C28DD-B602-4750-831D-345102A54CA0} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{14342787-B4EF-4076-8C91-BA6C523DE8DF} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

58
api/Avalonia.nupkg.xml

@ -114,6 +114,40 @@
<Target>M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetFrameThemeVariant(Avalonia.Platform.PlatformThemeVariant)</Target>
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.get_IsCompleted</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.GetAwaiter</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.GetResult</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.OnCompleted(System.Action)</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetAwaiter</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetResult</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
@ -193,6 +227,30 @@
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.Threading.DispatcherPriorityAwaitable</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.Threading.DispatcherPriorityAwaitable`1</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Threading.DispatcherPriorityAwaitable</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Threading.DispatcherPriorityAwaitable`1</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0009</DiagnosticId>
<Target>T:Avalonia.Diagnostics.StyleDiagnostics</Target>

4
azure-pipelines.yml

@ -80,7 +80,7 @@ jobs:
displayName: 'Install Workloads'
inputs:
script: |
dotnet workload install android ios macos wasm-tools
dotnet workload install android ios maccatalyst macos wasm-tools
- task: CmdLine@2
displayName: 'Generate avalonia-native'
@ -154,7 +154,7 @@ jobs:
displayName: 'Install Workloads'
inputs:
script: |
dotnet workload install android ios tvos wasm-tools
dotnet workload install android maccatalyst ios tvos wasm-tools
- task: CmdLine@2
displayName: 'Install Nuke'

14
build/AnalyzerProject.targets

@ -0,0 +1,14 @@
<Project>
<PropertyGroup>
<EnforceExtendedAnalyzerRules Condition="'$(EnforceExtendedAnalyzerRules)' == ''">true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent Condition="'$(IsRoslynComponent)' == ''">true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.5.0" PrivateAssets="all" />
</ItemGroup>
</Project>

1
build/TargetFrameworks.props

@ -4,6 +4,7 @@
<AvsCurrentWindowsTargetFramework>$(AvsCurrentTargetFramework)-windows</AvsCurrentWindowsTargetFramework>
<AvsCurrentMacOSTargetFramework>$(AvsCurrentTargetFramework)-macos</AvsCurrentMacOSTargetFramework>
<AvsCurrentAndroidTargetFramework>$(AvsCurrentTargetFramework)-android34.0</AvsCurrentAndroidTargetFramework>
<AvsCurrentMacCatalystTargetFramework>$(AvsCurrentTargetFramework)-maccatalyst17.0</AvsCurrentMacCatalystTargetFramework>
<AvsCurrentIOSTargetFramework>$(AvsCurrentTargetFramework)-ios17.0</AvsCurrentIOSTargetFramework>
<AvsCurrentTvOSTargetFramework>$(AvsCurrentTargetFramework)-tvos17.0</AvsCurrentTvOSTargetFramework>
<AvsCurrentBrowserTargetFramework>$(AvsCurrentTargetFramework)-browser</AvsCurrentBrowserTargetFramework>

25
docs/build.md

@ -16,7 +16,7 @@ Go to https://dotnet.microsoft.com/en-us/download/visual-studio-sdks and install
Since Avalonia targets pretty much every supported .NET platform, you need to install these workloads as well.
Running it from the command line:
```bash
dotnet workload install android ios wasm-tools
dotnet workload install android ios maccatalyst wasm-tools
```
macOS workloads are not required to build Avalonia.
@ -97,25 +97,6 @@ On macOS it is necessary to build and manually install the respective native lib
./build.sh CompileNative
```
# Building Avalonia into a local NuGet cache
It is possible to build Avalonia locally and generate NuGet packages that can be used locally to test local changes.
First, install Nuke's dotnet global tool like so:
```bash
dotnet tool install Nuke.GlobalTool --global
```
Then you need to run:
```bash
nuke --target BuildToNuGetCache --configuration Release
```
This command will generate nuget packages and push them into a local NuGet automatically.
To use these packages use `9999.0.0-localbuild` package version.
Each time local changes are made to Avalonia, running this command again will replace old packages and reset cache for the same version.
## Browser
To build and run browser/wasm projects, it's necessary to install NodeJS.
@ -124,3 +105,7 @@ You can find latest LTS on https://nodejs.org/.
## Windows
It is possible to run some .NET Framework samples and tests using .NET Framework SDK. You need to install at least 4.7 SDK.
## Building Avalonia into a local NuGet cache
See [Building Local NuGet Packages](nuget.md)

1
docs/index.md

@ -12,6 +12,7 @@ This documentation covers Avalonia framework development. For user documentation
- [Debugging the XAML Compiler](debug-xaml-compiler.md)
- [Porting Code from 3rd Party Sources](porting-code-from-3rd-party-sources.md)
- [Building Local NuGet Packages](nuget.md)
## Releases

56
docs/nuget.md

@ -0,0 +1,56 @@
# Building Local NuGet Packages
To build NuGet packages, one can use the `CreateNugetPackages` target:
Windows
```
.\build.ps1 CreateNugetPackages
```
Linux/macOS
```
./build.sh CreateNugetPackages
```
Or if you have Nuke's [dotnet global tool](https://nuke.build/docs/getting-started/installation/) installed:
```
nuke CreateNugetPackages
```
The produced NuGet packages will be placed in the `artifacts\nuget` directory.
> [!NOTE]
> The rest of this document will assume that you have the Nuke global tool installed, as the invocation is the same on all platforms. You can always replace `nuke` in the instructions below with the `build` script relvant to your platform.
By default the packages will be built in debug configuration. To build in relase configuration add the `--configuration` parameter, e.g.:
```
nuke CreateNugetPackages --configuration Release
```
To configure the version of the built packages, add the `--force-nuget-version` parameter, e.g.:
```
nuke CreateNugetPackages --force-nuget-version 11.4.0
```
## Building to the Local Cache
Building packages with the `CreateNugetPackages` target has a few gotchas:
- One needs to set up a local nuget feed to consume the packages
- When building on an operating system other than macOS, the Avalonia.Native package will not be built, resulting in a NuGet error when trying to use Avalonia.Desktop
- It's easy to introduce versioning problems
For these reasons, it is possible to build Avalonia directly to your machine's NuGet cache using the `BuildToNuGetCache` target:
```bash
nuke --target BuildToNuGetCache --configuration Release
```
This command will generate nuget packages and push them into your local NuGet cache (usually `~/.nuget/packages`) with a version of `9999.0.0-localbuild`.
Each time local changes are made to Avalonia, running this command again will replace the old packages and reset the cache, meaning that the changes should be picked up automatically by msbuild.

8
docs/release.md

@ -16,8 +16,10 @@ This document describes the process for creating a new Avalonia release
- Create a branch named e.g. `release/11.0.9` for the specific minor version
- Update the version number in the file [SharedVersion.props](../build/SharedVersion.props), e.g. `<Version>11.0.9</Version>`
- Add a tag for this version, e.g. `git tag 11.0.9`
- Push the release branch and the tag.
- Commit the file.
- Add a tag for this version, e.g. `git tag 11.0.9`.
- Update the `release/latest` branch to point to the same commit.
- Push the release branches and the tag.
- Wait for azure pipelines to finish the build. Nightly build with 11.0.9 version should be released soon after.
- Using the nightly build run a due diligence test to make sure you're happy with the package.
- On azure pipelines, on the release for your release branch `release/11.0.9` click on the badge for "Nuget Release"
@ -27,4 +29,4 @@ This document describes the process for creating a new Avalonia release
- Replace changelog with one generated by avalonia-backport tool. Enable discussion for the specific release
- Review the release information and publish.
- Update the dotnet templates, visual studio templates.
- Announce on telegram (RU and EN), twitter, etc
- Announce on telegram (RU and EN), twitter, etc

8
native/Avalonia.Native/src/OSX/WindowImpl.h

@ -45,6 +45,10 @@ BEGIN_INTERFACE_MAP()
void DoZoom();
virtual HRESULT SetCanResize(bool value) override;
virtual HRESULT SetCanMinimize(bool value) override;
virtual HRESULT SetCanMaximize(bool value) override;
virtual HRESULT SetDecorations(SystemDecorations value) override;
@ -82,7 +86,7 @@ BEGIN_INTERFACE_MAP()
bool CanBecomeKeyWindow ();
bool CanZoom() override { return _isEnabled && _canResize; }
bool CanZoom() override { return _isEnabled && _canMaximize; }
protected:
virtual NSWindowStyleMask CalculateStyleMask() override;
@ -94,6 +98,8 @@ private:
NSString *_lastTitle;
bool _isEnabled;
bool _canResize;
bool _canMinimize;
bool _canMaximize;
bool _fullScreenActive;
SystemDecorations _decorations;
AvnWindowState _lastWindowState;

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

@ -16,6 +16,8 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events) : TopLevelImpl(events), WindowB
_extendClientHints = AvnDefaultChrome;
_fullScreenActive = false;
_canResize = true;
_canMinimize = true;
_canMaximize = true;
_decorations = SystemDecorationsFull;
_transitioningWindowState = false;
_inSetWindowState = false;
@ -191,7 +193,8 @@ bool WindowImpl::IsZoomed() {
void WindowImpl::DoZoom() {
if (_decorations == SystemDecorationsNone ||
_decorations == SystemDecorationsBorderOnly ||
_canResize == false) {
_canResize == false ||
_canMaximize == false) {
[Window setFrame:[Window screen].visibleFrame display:true];
} else {
[Window performZoom:Window];
@ -208,6 +211,22 @@ HRESULT WindowImpl::SetCanResize(bool value) {
}
}
HRESULT WindowImpl::SetCanMinimize(bool value) {
START_COM_ARP_CALL;
_canMinimize = value;
UpdateAppearance();
return S_OK;
}
HRESULT WindowImpl::SetCanMaximize(bool value) {
START_COM_ARP_CALL;
_canMaximize = value;
UpdateAppearance();
return S_OK;
}
HRESULT WindowImpl::SetDecorations(SystemDecorations value) {
START_COM_CALL;
@ -583,7 +602,7 @@ NSWindowStyleMask WindowImpl::CalculateStyleMask() {
break;
}
if (!IsOwned()) {
if (_canMinimize && !IsOwned()) {
s |= NSWindowStyleMaskMiniaturizable;
}
@ -611,9 +630,9 @@ void WindowImpl::UpdateAppearance() {
[closeButton setHidden:!hasTrafficLights];
[closeButton setEnabled:_isEnabled];
[miniaturizeButton setHidden:!hasTrafficLights];
[miniaturizeButton setEnabled:_isEnabled];
[miniaturizeButton setEnabled:_isEnabled && _canMinimize];
[zoomButton setHidden:!hasTrafficLights];
[zoomButton setEnabled:CanZoom()];
[zoomButton setEnabled:CanZoom() || (([Window styleMask] & NSWindowStyleMaskFullScreen) != 0 && _isEnabled)];
}
extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events)

19
samples/ControlCatalog.iOS/ControlCatalog.MacCatalyst.csproj

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<ProvisioningType>manual</ProvisioningType>
<TargetFramework>$(AvsCurrentMacCatalystTargetFramework)</TargetFramework>
<!-- Used to support Desktop Mode Idiom, min supported version is 13.1, which supports iPad scaling. -->
<SupportedOSPlatformVersion>14.0</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
<None Include="Info.Catalyst.plist">
<LogicalName>Info.plist</LogicalName>
</None>
</ItemGroup>
<PropertyGroup>
<UseInterpreter>true</UseInterpreter>
</PropertyGroup>
</Project>

9
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

@ -3,11 +3,16 @@
<OutputType>Exe</OutputType>
<ProvisioningType>manual</ProvisioningType>
<TargetFramework>$(AvsCurrentIOSTargetFramework)</TargetFramework>
<!-- <TargetFramework>$(AvsCurrentTvOSTargetFramework)</TargetFramework>-->
<SupportedOSPlatformVersion>$(AvsMinSupportedIOSVersion)</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
<None Include="Info.iOS.plist">
<LogicalName>Info.plist</LogicalName>
</None>
</ItemGroup>
</Project>
<PropertyGroup>
<UseInterpreter>true</UseInterpreter>
</PropertyGroup>
</Project>

21
samples/ControlCatalog.iOS/ControlCatalog.tvOS.csproj

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<ProvisioningType>manual</ProvisioningType>
<TargetFramework>$(AvsCurrentTvOSTargetFramework)</TargetFramework>
<SupportedOSPlatformVersion>$(AvsMinSupportedTvOSVersion)</SupportedOSPlatformVersion>
<!-- To run this in the simulator, you need to use the x64 architecture,
since SkiaSharp only bundles native libraries for x64 for the tvOS Simulator -->
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">tvossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
<None Include="Info.tvOS.plist">
<LogicalName>Info.plist</LogicalName>
</None>
</ItemGroup>
<PropertyGroup>
<UseInterpreter>true</UseInterpreter>
</PropertyGroup>
</Project>

42
samples/ControlCatalog.iOS/Info.Catalyst.plist

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>ControlCatalog.Catalyst</string>
<key>CFBundleIdentifier</key>
<string>Avalonia.ControlCatalog</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>6</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

3
samples/ControlCatalog.iOS/Info.plist → samples/ControlCatalog.iOS/Info.iOS.plist

@ -16,7 +16,6 @@
<array>
<integer>1</integer>
<integer>2</integer>
<integer>3</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
@ -38,5 +37,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

40
samples/ControlCatalog.iOS/Info.tvOS.plist

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>ControlCatalog.tvOS</string>
<key>CFBundleIdentifier</key>
<string>Avalonia.ControlCatalog</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>3</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

3
samples/ControlCatalog/MainWindow.xaml

@ -10,6 +10,9 @@
ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}"
ExtendClientAreaChromeHints="{Binding ChromeHints}"
ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}"
CanResize="{Binding CanResize}"
CanMinimize="{Binding CanMinimize}"
CanMaximize="{Binding CanMaximize}"
x:Name="MainWindow"
Background="Transparent"
x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"

5
samples/ControlCatalog/Pages/CalendarPage.xaml

@ -32,6 +32,11 @@
<TextBlock Text="SelectionMode: MultipleRange"/>
<Calendar SelectionMode="MultipleRange" />
</StackPanel>
<StackPanel>
<TextBlock Text="Tap Range Selection" />
<Calendar SelectionMode="SingleRange"
AllowTapRangeSelection="True" />
</StackPanel>
<StackPanel>
<TextBlock Text="DisplayDates"/>
<Calendar Name="DisplayDatesCalendar"

14
samples/ControlCatalog/Pages/ComboBoxPage.xaml

@ -124,7 +124,7 @@
</ComboBox.SelectionBoxItemTemplate>
</ComboBox>
<ComboBox WrapSelection="{Binding WrapSelection}" ItemsSource="{Binding Values}" >
<ComboBox WrapSelection="{Binding WrapSelection}" ItemsSource="{Binding Values}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
@ -134,6 +134,18 @@
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<StackPanel Spacing="5">
<ComboBox WrapSelection="{Binding WrapSelection}" PlaceholderText="Editable"
ItemsSource="{Binding Values}" DisplayMemberBinding="{Binding Name}"
IsEditable="True" Text="{Binding TextValue}"
TextSearch.TextBinding="{Binding SearchText, DataType=viewModels:IdAndName}"
SelectedItem="{Binding SelectedItem}" />
<TextBlock Text="Editable text is bound to SearchText. Display is bound to Name" />
<TextBlock Text="{Binding TextValue, StringFormat=Text Value: {0}}" />
<TextBlock Text="{Binding SelectedItem.Name, StringFormat=Selected Item: {0}}" />
</StackPanel>
</WrapPanel>
</StackPanel>
</StackPanel>

3
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -20,6 +20,9 @@
<Setter Property="Background" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="VirtualizingStackPanel">
<Setter Property="CacheLength" Value="0.5" />
</Style>
</DockPanel.Styles>
<StackPanel DockPanel.Dock="Top" Margin="4">
<TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock>

49
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@ -8,18 +8,55 @@
x:DataType="viewModels:MainWindowViewModel"
x:CompileBindings="True">
<StackPanel>
<StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Desktop=true}">
<TextBlock Classes="h2" Text="Desktop properties" Margin="4" />
<CheckBox Content="Extend Client Area to Decorations" IsChecked="{Binding ExtendClientAreaEnabled}" />
<CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" />
<CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" />
<Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" />
<StackPanel
Spacing="10"
Margin="25"
IsEnabled="{OnFormFactor false, Desktop=true}">
<TextBlock Classes="h2"
Text="Desktop properties"
Margin="4" />
<CheckBox Content="Extend Client Area to Decorations"
IsChecked="{Binding ExtendClientAreaEnabled}" />
<DockPanel IsEnabled="{Binding ExtendClientAreaEnabled}">
<CheckBox Content="Title Bar"
IsChecked="{Binding SystemTitleBarEnabled}"
DockPanel.Dock="Left" />
<Slider Minimum="-1"
Maximum="200"
Value="{Binding TitleBarHeight}"
IsEnabled="{Binding SystemTitleBarEnabled}"
Margin="8,-10" />
</DockPanel>
<CheckBox Content="Prefer System Chrome"
IsChecked="{Binding PreferSystemChromeEnabled}"
IsEnabled="{Binding ExtendClientAreaEnabled}" />
<CheckBox Content="Can Resize"
IsChecked="{Binding CanResize}" />
<CheckBox Content="Can Minimize"
IsChecked="{Binding CanMinimize}" />
<CheckBox Content="Can Maximize"
IsChecked="{Binding CanMaximize}"
IsEnabled="{Binding CanResize}" />
</StackPanel>
<StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Mobile=true}">
<TextBlock Classes="h2" Text="Mobile properties" Margin="4" />
<CheckBox Content="Is System Bar Visible" IsChecked="{Binding IsSystemBarVisible}" />
<CheckBox Content="Display Edge To Edge" IsChecked="{Binding DisplayEdgeToEdge}" />
<TextBlock Text="{Binding SafeAreaPadding, StringFormat='Safe Area Padding: {0}'}" />
</StackPanel>
</StackPanel>
</UserControl>

14
samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs

@ -10,6 +10,8 @@ namespace ControlCatalog.ViewModels
public class ComboBoxPageViewModel : ViewModelBase
{
private bool _wrapSelection;
private string _textValue = string.Empty;
private IdAndName? _selectedItem = null;
public bool WrapSelection
{
@ -17,6 +19,18 @@ namespace ControlCatalog.ViewModels
set => this.RaiseAndSetIfChanged(ref _wrapSelection, value);
}
public string TextValue
{
get => _textValue;
set => this.RaiseAndSetIfChanged(ref _textValue, value);
}
public IdAndName? SelectedItem
{
get => _selectedItem;
set => this.RaiseAndSetIfChanged(ref _selectedItem, value);
}
public ObservableCollection<IdAndName> Values { get; set; } = new ObservableCollection<IdAndName>
{
new IdAndName(){ Id = "Id 1", Name = "Name 1", SearchText = "A" },

64
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -1,6 +1,5 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications;
using Avalonia.Dialogs;
using Avalonia.Platform;
using System;
@ -22,6 +21,9 @@ namespace ControlCatalog.ViewModels
private bool _isSystemBarVisible;
private bool _displayEdgeToEdge;
private Thickness _safeAreaPadding;
private bool _canResize;
private bool _canMinimize;
private bool _canMaximize;
public MainWindowViewModel()
{
@ -49,7 +51,7 @@ namespace ControlCatalog.ViewModels
WindowState.FullScreen,
};
this.PropertyChanged += (s, e) =>
PropertyChanged += (s, e) =>
{
if (e.PropertyName is nameof(SystemTitleBarEnabled) or nameof(PreferSystemChromeEnabled))
{
@ -67,70 +69,104 @@ namespace ControlCatalog.ViewModels
}
};
SystemTitleBarEnabled = true;
SystemTitleBarEnabled = true;
TitleBarHeight = -1;
CanResize = true;
CanMinimize = true;
CanMaximize = true;
}
public ExtendClientAreaChromeHints ChromeHints
{
get { return _chromeHints; }
set { this.RaiseAndSetIfChanged(ref _chromeHints, value); }
set { RaiseAndSetIfChanged(ref _chromeHints, value); }
}
public bool ExtendClientAreaEnabled
{
get { return _extendClientAreaEnabled; }
set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); }
set
{
if (RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value) && !value)
{
SystemTitleBarEnabled = true;
}
}
}
public bool SystemTitleBarEnabled
{
get { return _systemTitleBarEnabled; }
set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); }
set
{
if (RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value) && !value)
{
TitleBarHeight = -1;
}
}
}
public bool PreferSystemChromeEnabled
{
get { return _preferSystemChromeEnabled; }
set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); }
set { RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); }
}
public double TitleBarHeight
{
get { return _titleBarHeight; }
set { this.RaiseAndSetIfChanged(ref _titleBarHeight, value); }
set { RaiseAndSetIfChanged(ref _titleBarHeight, value); }
}
public WindowState WindowState
{
get { return _windowState; }
set { this.RaiseAndSetIfChanged(ref _windowState, value); }
set { RaiseAndSetIfChanged(ref _windowState, value); }
}
public WindowState[] WindowStates
{
get { return _windowStates; }
set { this.RaiseAndSetIfChanged(ref _windowStates, value); }
set { RaiseAndSetIfChanged(ref _windowStates, value); }
}
public bool IsSystemBarVisible
{
get { return _isSystemBarVisible; }
set { this.RaiseAndSetIfChanged(ref _isSystemBarVisible, value); }
set { RaiseAndSetIfChanged(ref _isSystemBarVisible, value); }
}
public bool DisplayEdgeToEdge
{
get { return _displayEdgeToEdge; }
set { this.RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); }
set { RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); }
}
public Thickness SafeAreaPadding
{
get { return _safeAreaPadding; }
set { this.RaiseAndSetIfChanged(ref _safeAreaPadding, value); }
set { RaiseAndSetIfChanged(ref _safeAreaPadding, value); }
}
public bool CanResize
{
get { return _canResize; }
set { RaiseAndSetIfChanged(ref _canResize, value); }
}
public bool CanMinimize
{
get { return _canMinimize; }
set { RaiseAndSetIfChanged(ref _canMinimize, value); }
}
public bool CanMaximize
{
get { return _canMaximize; }
set { RaiseAndSetIfChanged(ref _canMaximize, value); }
}
public MiniCommand AboutCommand { get; }
public MiniCommand ExitCommand { get; }
@ -144,7 +180,7 @@ namespace ControlCatalog.ViewModels
public DateTime? ValidatedDateExample
{
get => _validatedDateExample;
set => this.RaiseAndSetIfChanged(ref _validatedDateExample, value);
set => RaiseAndSetIfChanged(ref _validatedDateExample, value);
}
}
}

2
samples/Generators.Sandbox/Controls/SignUpView.xaml

@ -8,7 +8,7 @@
Watermark="Please, enter user name..."
UseFloatingWatermark="True" />
<TextBlock x:Name="UserNameValidation"
Foreground="Red"
Foreground="Green"
FontSize="12" />
<TextBox Margin="0 10 0 0"
x:Name="PasswordTextBox"

6
samples/IntegrationTestApp/Pages/WindowPage.axaml

@ -30,14 +30,16 @@
</ComboBox>
<CheckBox Name="ShowWindowExtendClientAreaToDecorationsHint">ExtendClientAreaToDecorationsHint</CheckBox>
<CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox>
<CheckBox Name="ShowWindowCanMinimize" IsChecked="True">Can Minimize</CheckBox>
<CheckBox Name="ShowWindowCanMaximize" IsChecked="True">Can Maximize</CheckBox>
</StackPanel>
<StackPanel Grid.Column="2">
<Button Name="ShowWindow" Click="ShowWindow_Click">Show Window</Button>
<Button Name="SendToBack" Click="SendToBack_Click">Send to Back</Button>
<Button Name="EnterFullscreen" Click="EnterFullscreen_Click">Enter Fullscreen</Button>
<Button Name="ExitFullscreen" Click="ExitFullscreen_Click">Exit Fullscreen</Button>
<Button Name="RestoreAll" Click="RestoreAll_Click">Restore All</Button>
<Button Name="ShowTopmostWindow" Click="ShowTopmostWindow_Click">Show Topmost Window</Button>
</StackPanel>
<StackPanel Grid.Column="2">
<Button Name="ShowTransparentWindow" Click="ShowTransparentWindow_Click">Transparent Window</Button>
<Button Name="ShowTransparentPopup" Click="ShowTransparentPopup_Click">Transparent Popup</Button>
</StackPanel>

5
samples/IntegrationTestApp/Pages/WindowPage.axaml.cs

@ -23,10 +23,13 @@ public partial class WindowPage : UserControl
private void ShowWindow_Click(object? sender, RoutedEventArgs e)
{
var size = !string.IsNullOrWhiteSpace(ShowWindowSize.Text) ? Size.Parse(ShowWindowSize.Text) : (Size?)null;
var canResize = ShowWindowCanResize.IsChecked ?? false;
var window = new ShowWindowTest
{
WindowStartupLocation = (WindowStartupLocation)ShowWindowLocation.SelectedIndex,
CanResize = ShowWindowCanResize.IsChecked ?? false,
CanResize = canResize,
CanMinimize = ShowWindowCanMinimize.IsChecked ?? false,
CanMaximize = canResize && (ShowWindowCanMaximize.IsChecked ?? false)
};
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)

5
samples/TextTestApp/App.axaml

@ -0,0 +1,5 @@
<Application xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="TextTestApp.App" RequestedThemeVariant="Light">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

21
samples/TextTestApp/App.axaml.cs

@ -0,0 +1,21 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace TextTestApp
{
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow = new MainWindow();
base.OnFrameworkInitializationCompleted();
}
}
}

24
samples/TextTestApp/GridRow.cs

@ -0,0 +1,24 @@
using System.Collections.Specialized;
using Avalonia.Controls;
using Avalonia.Layout;
namespace TextTestApp
{
public class GridRow : Grid
{
protected override void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
base.ChildrenChanged(sender, e);
while (Children.Count > ColumnDefinitions.Count)
ColumnDefinitions.Add(new ColumnDefinition { SharedSizeGroup = "c" + ColumnDefinitions.Count });
for (int i = 0; i < Children.Count; i++)
{
SetColumn(Children[i], i);
if (Children[i] is Layoutable l)
l.VerticalAlignment = VerticalAlignment.Center;
}
}
}
}

705
samples/TextTestApp/InteractiveLineControl.cs

@ -0,0 +1,705 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
namespace TextTestApp
{
public class InteractiveLineControl : Control
{
/// <summary>
/// Defines the <see cref="Text" /> property.
/// </summary>
public static readonly StyledProperty<string?> TextProperty =
TextBlock.TextProperty.AddOwner<InteractiveLineControl>();
/// <summary>
/// Defines the <see cref="Background"/> property.
/// </summary>
public static readonly StyledProperty<IBrush?> BackgroundProperty =
Border.BackgroundProperty.AddOwner<InteractiveLineControl>();
public static readonly StyledProperty<IBrush?> ExtentStrokeProperty =
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(ExtentStroke));
public static readonly StyledProperty<IBrush?> BaselineStrokeProperty =
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(BaselineStroke));
public static readonly StyledProperty<IBrush?> TextBoundsStrokeProperty =
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(TextBoundsStroke));
public static readonly StyledProperty<IBrush?> RunBoundsStrokeProperty =
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(RunBoundsStroke));
public static readonly StyledProperty<IBrush?> NextHitStrokeProperty =
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(NextHitStroke));
public static readonly StyledProperty<IBrush?> BackspaceHitStrokeProperty =
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(BackspaceHitStroke));
public static readonly StyledProperty<IBrush?> PreviousHitStrokeProperty =
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(PreviousHitStroke));
public static readonly StyledProperty<IBrush?> DistanceStrokeProperty =
AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(DistanceStroke));
public IBrush? ExtentStroke
{
get => GetValue(ExtentStrokeProperty);
set => SetValue(ExtentStrokeProperty, value);
}
public IBrush? BaselineStroke
{
get => GetValue(BaselineStrokeProperty);
set => SetValue(BaselineStrokeProperty, value);
}
public IBrush? TextBoundsStroke
{
get => GetValue(TextBoundsStrokeProperty);
set => SetValue(TextBoundsStrokeProperty, value);
}
public IBrush? RunBoundsStroke
{
get => GetValue(RunBoundsStrokeProperty);
set => SetValue(RunBoundsStrokeProperty, value);
}
public IBrush? NextHitStroke
{
get => GetValue(NextHitStrokeProperty);
set => SetValue(NextHitStrokeProperty, value);
}
public IBrush? BackspaceHitStroke
{
get => GetValue(BackspaceHitStrokeProperty);
set => SetValue(BackspaceHitStrokeProperty, value);
}
public IBrush? PreviousHitStroke
{
get => GetValue(PreviousHitStrokeProperty);
set => SetValue(PreviousHitStrokeProperty, value);
}
public IBrush? DistanceStroke
{
get => GetValue(DistanceStrokeProperty);
set => SetValue(DistanceStrokeProperty, value);
}
private IPen? _extentPen;
protected IPen ExtentPen => _extentPen ??= new Pen(ExtentStroke, dashStyle: DashStyle.Dash);
private IPen? _baselinePen;
protected IPen BaselinePen => _baselinePen ??= new Pen(BaselineStroke);
private IPen? _textBoundsPen;
protected IPen TextBoundsPen => _textBoundsPen ??= new Pen(TextBoundsStroke);
private IPen? _runBoundsPen;
protected IPen RunBoundsPen => _runBoundsPen ??= new Pen(RunBoundsStroke, dashStyle: DashStyle.Dash);
private IPen? _nextHitPen;
protected IPen NextHitPen => _nextHitPen ??= new Pen(NextHitStroke);
private IPen? _previousHitPen;
protected IPen PreviousHitPen => _previousHitPen ??= new Pen(PreviousHitStroke);
private IPen? _backspaceHitPen;
protected IPen BackspaceHitPen => _backspaceHitPen ??= new Pen(BackspaceHitStroke);
private IPen? _distancePen;
protected IPen DistancePen => _distancePen ??= new Pen(DistanceStroke);
/// <summary>
/// Gets or sets the text to draw.
/// </summary>
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
/// <summary>
/// Gets or sets a brush used to paint the control's background.
/// </summary>
public IBrush? Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
// TextRunProperties
/// <summary>
/// Defines the <see cref="FontFamily"/> property.
/// </summary>
public static readonly StyledProperty<FontFamily> FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner<InteractiveLineControl>();
/// <summary>
/// Defines the <see cref="FontFeaturesProperty"/> property.
/// </summary>
public static readonly StyledProperty<FontFeatureCollection?> FontFeaturesProperty =
TextElement.FontFeaturesProperty.AddOwner<InteractiveLineControl>();
/// <summary>
/// Defines the <see cref="FontSize"/> property.
/// </summary>
public static readonly StyledProperty<double> FontSizeProperty =
TextElement.FontSizeProperty.AddOwner<InteractiveLineControl>();
/// <summary>
/// Defines the <see cref="FontStyle"/> property.
/// </summary>
public static readonly StyledProperty<FontStyle> FontStyleProperty =
TextElement.FontStyleProperty.AddOwner<InteractiveLineControl>();
/// <summary>
/// Defines the <see cref="FontWeight"/> property.
/// </summary>
public static readonly StyledProperty<FontWeight> FontWeightProperty =
TextElement.FontWeightProperty.AddOwner<InteractiveLineControl>();
/// <summary>
/// Defines the <see cref="FontWeight"/> property.
/// </summary>
public static readonly StyledProperty<FontStretch> FontStretchProperty =
TextElement.FontStretchProperty.AddOwner<InteractiveLineControl>();
/// <summary>
/// Gets or sets the font family used to draw the control's text.
/// </summary>
public FontFamily FontFamily
{
get => GetValue(FontFamilyProperty);
set => SetValue(FontFamilyProperty, value);
}
/// <summary>
/// Gets or sets the font features turned on/off.
/// </summary>
public FontFeatureCollection? FontFeatures
{
get => GetValue(FontFeaturesProperty);
set => SetValue(FontFeaturesProperty, value);
}
/// <summary>
/// Gets or sets the size of the control's text in points.
/// </summary>
public double FontSize
{
get => GetValue(FontSizeProperty);
set => SetValue(FontSizeProperty, value);
}
/// <summary>
/// Gets or sets the font style used to draw the control's text.
/// </summary>
public FontStyle FontStyle
{
get => GetValue(FontStyleProperty);
set => SetValue(FontStyleProperty, value);
}
/// <summary>
/// Gets or sets the font weight used to draw the control's text.
/// </summary>
public FontWeight FontWeight
{
get => GetValue(FontWeightProperty);
set => SetValue(FontWeightProperty, value);
}
/// <summary>
/// Gets or sets the font stretch used to draw the control's text.
/// </summary>
public FontStretch FontStretch
{
get => GetValue(FontStretchProperty);
set => SetValue(FontStretchProperty, value);
}
private GenericTextRunProperties? _textRunProperties;
public GenericTextRunProperties TextRunProperties
{
get
{
return _textRunProperties ??= CreateTextRunProperties();
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
_textRunProperties = value;
SetCurrentValue(FontFamilyProperty, value.Typeface.FontFamily);
SetCurrentValue(FontFeaturesProperty, value.FontFeatures);
SetCurrentValue(FontSizeProperty, value.FontRenderingEmSize);
SetCurrentValue(FontStyleProperty, value.Typeface.Style);
SetCurrentValue(FontWeightProperty, value.Typeface.Weight);
SetCurrentValue(FontStretchProperty, value.Typeface.Stretch);
}
}
private GenericTextRunProperties CreateTextRunProperties()
{
Typeface typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
return new GenericTextRunProperties(typeface, FontFeatures, FontSize,
textDecorations: null,
foregroundBrush: Brushes.Black,
backgroundBrush: null,
baselineAlignment: BaselineAlignment.Baseline,
cultureInfo: null);
}
// TextParagraphProperties
private GenericTextParagraphProperties? _textParagraphProperties;
public GenericTextParagraphProperties TextParagraphProperties
{
get
{
return _textParagraphProperties ??= CreateTextParagraphProperties();
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
_textParagraphProperties = null;
SetCurrentValue(FlowDirectionProperty, value.FlowDirection);
}
}
private GenericTextParagraphProperties CreateTextParagraphProperties()
{
return new GenericTextParagraphProperties(
FlowDirection,
TextAlignment.Start,
firstLineInParagraph: false,
alwaysCollapsible: false,
TextRunProperties,
textWrapping: TextWrapping.NoWrap,
lineHeight: 0,
indent: 0,
letterSpacing: 0);
}
private readonly ITextSource _textSource;
private class TextSource : ITextSource
{
private readonly InteractiveLineControl _owner;
public TextSource(InteractiveLineControl owner)
{
_owner = owner;
}
public TextRun? GetTextRun(int textSourceIndex)
{
string text = _owner.Text ?? string.Empty;
if (textSourceIndex < 0 || textSourceIndex >= text.Length)
return null;
return new TextCharacters(text, _owner.TextRunProperties);
}
}
private TextLine? _textLine;
public TextLine? TextLine => _textLine ??= TextFormatter.Current.FormatLine(_textSource, 0, Bounds.Size.Width, TextParagraphProperties);
private TextLayout? _textLayout;
public TextLayout TextLayout => _textLayout ??= new TextLayout(_textSource, TextParagraphProperties);
private Size? _textLineSize;
protected Size TextLineSize => _textLineSize ??= TextLine is { } textLine ? new Size(textLine.WidthIncludingTrailingWhitespace, textLine.Height) : default;
private Size? _inkSize;
protected Size InkSize => _inkSize ??= TextLine is { } textLine ? new Size(textLine.OverhangLeading + textLine.WidthIncludingTrailingWhitespace + textLine.OverhangTrailing, textLine.Extent) : default;
public event EventHandler? TextLineChanged;
public InteractiveLineControl()
{
_textSource = new TextSource(this);
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
RenderOptions.SetTextRenderingMode(this, TextRenderingMode.SubpixelAntialias);
}
private void InvalidateTextRunProperties()
{
_textRunProperties = null;
InvalidateTextParagraphProperties();
}
private void InvalidateTextParagraphProperties()
{
_textParagraphProperties = null;
InvalidateTextLine();
}
private void InvalidateTextLine()
{
_textLayout = null;
_textLine = null;
_textLineSize = null;
_inkSize = null;
InvalidateMeasure();
InvalidateVisual();
TextLineChanged?.Invoke(this, EventArgs.Empty);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(FontFamily):
case nameof(FontSize):
InvalidateTextRunProperties();
break;
case nameof(FontStyle):
case nameof(FontWeight):
case nameof(FontStretch):
InvalidateTextRunProperties();
break;
case nameof(FlowDirection):
InvalidateTextParagraphProperties();
break;
case nameof(Text):
InvalidateTextLine();
break;
case nameof(BaselineStroke):
_baselinePen = null;
InvalidateVisual();
break;
case nameof(TextBoundsStroke):
_textBoundsPen = null;
InvalidateVisual();
break;
case nameof(RunBoundsStroke):
_runBoundsPen = null;
InvalidateVisual();
break;
case nameof(NextHitStroke):
_nextHitPen = null;
InvalidateVisual();
break;
case nameof(PreviousHitStroke):
_previousHitPen = null;
InvalidateVisual();
break;
case nameof(BackspaceHitStroke):
_backspaceHitPen = null;
InvalidateVisual();
break;
}
base.OnPropertyChanged(change);
}
protected override Size MeasureOverride(Size availableSize)
{
if (TextLine == null)
return default;
return new Size(Math.Max(TextLineSize.Width, InkSize.Width), Math.Max(TextLineSize.Height, InkSize.Height));
}
private const double VerticalSpacing = 5;
private const double HorizontalSpacing = 5;
private const double ArrowSize = 5;
private Dictionary<string, FormattedText> _labelsCache = new();
protected FormattedText GetOrCreateLabel(string label, IBrush brush, bool disableCache = false)
{
if (_labelsCache.TryGetValue(label, out var text))
return text;
text = new FormattedText(label, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, Typeface.Default, 8, brush);
if (!disableCache)
_labelsCache[label] = text;
return text;
}
private Rect _inkRenderBounds;
private Rect _lineRenderBounds;
public Rect InkRenderBounds => _inkRenderBounds;
public Rect LineRenderBounds => _lineRenderBounds;
public override void Render(DrawingContext context)
{
TextLine? textLine = TextLine;
if (textLine == null)
return;
// overhang leading should be negative when extending (e.g. for j) WPF: "When the leading alignment point comes before the leading drawn pixel, the value is negative." - docs wrong but values correct
// overhang trailing should be negative when extending (e.g. for f) WPF: "The OverhangTrailing value will be positive when the trailing drawn pixel comes before the trailing alignment point."
// overhang after should be negative when inside (e.g. for x) WPF: "The value is positive if the bottommost drawn pixel goes below the line bottom, and is negative if it is within (on or above) the line."
// => we want overhang before to be negative when inside (e.g. for x)
double overhangBefore = textLine.Extent - textLine.OverhangAfter - textLine.Height;
Rect inkBounds = new Rect(new Point(textLine.OverhangLeading, -overhangBefore), InkSize);
Rect lineBounds = new Rect(new Point(0, 0), TextLineSize);
if (inkBounds.Left < 0)
lineBounds = lineBounds.Translate(new Vector(-inkBounds.Left, 0));
if (inkBounds.Top < 0)
lineBounds = lineBounds.Translate(new Vector(0, -inkBounds.Top));
_inkRenderBounds = inkBounds;
_lineRenderBounds = lineBounds;
Rect bounds = new Rect(0, 0, Math.Max(inkBounds.Right, lineBounds.Right), Math.Max(inkBounds.Bottom, lineBounds.Bottom));
double labelX = bounds.Right + HorizontalSpacing;
if (Background is IBrush background)
context.FillRectangle(background, lineBounds);
if (ExtentStroke != null)
{
context.DrawRectangle(ExtentPen, inkBounds);
RenderLabel(context, nameof(textLine.Extent), ExtentStroke, labelX, inkBounds.Top);
}
using (context.PushTransform(Matrix.CreateTranslation(lineBounds.Left, lineBounds.Top)))
{
labelX -= lineBounds.Left; // labels to ignore horizontal transform
if (BaselineStroke != null)
{
RenderFontLine(context, textLine.Baseline, lineBounds.Width, BaselinePen); // no other lines currently available in Avalonia
RenderLabel(context, nameof(textLine.Baseline), BaselineStroke, labelX, textLine.Baseline);
}
textLine.Draw(context, lineOrigin: default);
var runBoundsStroke = RunBoundsStroke;
if (TextBoundsStroke != null || runBoundsStroke != null)
{
IReadOnlyList<TextBounds> textBounds = textLine.GetTextBounds(textLine.FirstTextSourceIndex, textLine.Length);
foreach (var textBound in textBounds)
{
if (runBoundsStroke != null)
{
var runBounds = textBound.TextRunBounds;
foreach (var runBound in runBounds)
context.DrawRectangle(RunBoundsPen, runBound.Rectangle);
}
context.DrawRectangle(TextBoundsPen, textBound.Rectangle);
}
}
double y = inkBounds.Bottom - lineBounds.Top + VerticalSpacing * 2;
if (NextHitStroke != null)
{
RenderHits(context, NextHitPen, textLine, textLine.GetNextCaretCharacterHit, new CharacterHit(0), ref y);
RenderLabel(context, nameof(textLine.GetNextCaretCharacterHit), NextHitStroke, labelX, y);
y += VerticalSpacing * 2;
}
if (PreviousHitStroke != null)
{
RenderLabel(context, nameof(textLine.GetPreviousCaretCharacterHit), PreviousHitStroke, labelX, y);
RenderHits(context, PreviousHitPen, textLine, textLine.GetPreviousCaretCharacterHit, new CharacterHit(textLine.Length), ref y);
y += VerticalSpacing * 2;
}
if (BackspaceHitStroke != null)
{
RenderLabel(context, nameof(textLine.GetBackspaceCaretCharacterHit), BackspaceHitStroke, labelX, y);
RenderHits(context, BackspaceHitPen, textLine, textLine.GetBackspaceCaretCharacterHit, new CharacterHit(textLine.Length), ref y);
y += VerticalSpacing * 2;
}
if (DistanceStroke != null)
{
y += VerticalSpacing;
var label = RenderLabel(context, nameof(textLine.GetDistanceFromCharacterHit), DistanceStroke, 0, y);
y += label.Height;
for (int i = 0; i < textLine.Length; i++)
{
var hit = new CharacterHit(i);
CharacterHit prevHit = default, nextHit = default;
double leftLabelX = -HorizontalSpacing;
// we want z-order to be previous, next, distance
// but labels need to be ordered next, distance, previous
if (NextHitStroke != null)
{
nextHit = textLine.GetNextCaretCharacterHit(hit);
var nextLabel = RenderLabel(context, $" > {nextHit.FirstCharacterIndex}+{nextHit.TrailingLength}", NextHitStroke, leftLabelX, y, TextAlignment.Right, disableCache: true);
leftLabelX -= nextLabel.WidthIncludingTrailingWhitespace;
}
if (PreviousHitStroke != null)
{
prevHit = textLine.GetPreviousCaretCharacterHit(hit);
var x1 = textLine.GetDistanceFromCharacterHit(new CharacterHit(prevHit.FirstCharacterIndex, 0));
var x2 = textLine.GetDistanceFromCharacterHit(new CharacterHit(prevHit.FirstCharacterIndex + prevHit.TrailingLength, 0));
RenderHorizontalPoint(context, x1, x2, y, PreviousHitPen, ArrowSize);
}
if (NextHitStroke != null)
{
var x1 = textLine.GetDistanceFromCharacterHit(new CharacterHit(nextHit.FirstCharacterIndex, 0));
var x2 = textLine.GetDistanceFromCharacterHit(new CharacterHit(nextHit.FirstCharacterIndex + nextHit.TrailingLength, 0));
RenderHorizontalPoint(context, x1, x2, y, NextHitPen, ArrowSize);
}
label = RenderLabel(context, $"[{i}]", DistanceStroke, leftLabelX, y, TextAlignment.Right);
leftLabelX -= label.WidthIncludingTrailingWhitespace;
if (PreviousHitStroke != null)
RenderLabel(context, $"{prevHit.FirstCharacterIndex}+{prevHit.TrailingLength} < ", PreviousHitStroke, leftLabelX, y, TextAlignment.Right, disableCache: true);
double distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(i));
RenderHorizontalBar(context, 0, distance, y, DistancePen, ArrowSize);
//RenderLabel(context, distance.ToString("F2"), DistanceStroke, distance + HorizontalSpacing, y, disableCache: true);
y += label.Height;
}
}
}
}
[return: NotNullIfNotNull("brush")]
private FormattedText? RenderLabel(DrawingContext context, string label, IBrush? brush, double x, double y, TextAlignment alignment = TextAlignment.Left, bool disableCache = false)
{
if (brush == null)
return null;
var text = GetOrCreateLabel(label, brush, disableCache);
if (alignment == TextAlignment.Right)
context.DrawText(text, new Point(x - text.WidthIncludingTrailingWhitespace, y - text.Height / 2));
else
context.DrawText(text, new Point(x, y - text.Height / 2));
return text;
}
private void RenderHits(DrawingContext context, IPen hitPen, TextLine textLine, Func<CharacterHit, CharacterHit> nextHit, CharacterHit startingHit, ref double y)
{
CharacterHit lastHit = startingHit;
double lastX = textLine.GetDistanceFromCharacterHit(lastHit);
double lastDirection = 0;
y -= VerticalSpacing; // we always start with adding one below
while (true)
{
CharacterHit hit = nextHit(lastHit);
if (hit == lastHit)
break;
double x = textLine.GetDistanceFromCharacterHit(hit);
double direction = Math.Sign(x - lastX);
if (direction == 0 || lastDirection != direction)
y += VerticalSpacing;
if (direction == 0)
RenderPoint(context, x, y, hitPen, ArrowSize);
else
RenderHorizontalArrow(context, lastX, x, y, hitPen, ArrowSize);
lastX = x;
lastHit = hit;
lastDirection = direction;
}
}
private void RenderPoint(DrawingContext context, double x, double y, IPen pen, double arrowHeight)
{
context.DrawEllipse(pen.Brush, pen, new Point(x, y), ArrowSize / 2, ArrowSize / 2);
}
private void RenderHorizontalPoint(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size)
{
PathGeometry startCap = new PathGeometry();
PathFigure startFigure = new PathFigure();
startFigure.StartPoint = new Point(xStart, y - size / 2);
startFigure.IsClosed = true;
startFigure.IsFilled = true;
startFigure.Segments!.Add(new ArcSegment { Size = new Size(size / 2, size / 2), Point = new Point(xStart, y + size / 2), SweepDirection = SweepDirection.CounterClockwise });
startCap.Figures!.Add(startFigure);
context.DrawGeometry(pen.Brush, pen, startCap);
PathGeometry endCap = new PathGeometry();
PathFigure endFigure = new PathFigure();
endFigure.StartPoint = new Point(xEnd, y - size / 2);
endFigure.IsClosed = true;
endFigure.IsFilled = false;
endFigure.Segments!.Add(new ArcSegment { Size = new Size(size / 2, size / 2), Point = new Point(xEnd, y + size / 2), SweepDirection = SweepDirection.Clockwise });
endCap.Figures!.Add(endFigure);
context.DrawGeometry(pen.Brush, pen, endCap);
}
private void RenderHorizontalArrow(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size)
{
context.DrawLine(pen, new Point(xStart, y), new Point(xEnd, y));
context.DrawLine(pen, new Point(xStart, y - size / 2), new Point(xStart, y + size / 2)); // start cap
if (xEnd >= xStart)
context.DrawGeometry(pen.Brush, pen, new PolylineGeometry(
[
new Point(xEnd - size, y - size / 2),
new Point(xEnd - size, y + size/2),
new Point(xEnd, y)
], isFilled: true));
else
context.DrawGeometry(pen.Brush, pen, new PolylineGeometry(
[
new Point(xEnd + size, y - size / 2),
new Point(xEnd + size, y + size/2),
new Point(xEnd, y)
], isFilled: true));
}
private void RenderHorizontalBar(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size)
{
context.DrawLine(pen, new Point(xStart, y), new Point(xEnd, y));
context.DrawLine(pen, new Point(xStart, y - size / 2), new Point(xStart, y + size / 2)); // start cap
context.DrawLine(pen, new Point(xEnd, y - size / 2), new Point(xEnd, y + size / 2)); // end cap
}
private void RenderFontLine(DrawingContext context, double y, double width, IPen pen)
{
context.DrawLine(pen, new Point(0, y), new Point(width, y));
}
}
}

105
samples/TextTestApp/MainWindow.axaml

@ -0,0 +1,105 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TextTestApp"
x:Class="TextTestApp.MainWindow"
Title="Text Test App" Width="700" Height="700">
<DockPanel>
<Border DockPanel.Dock="Bottom" Background="WhiteSmoke" BorderThickness="0,1,0,0" BorderBrush="Silver" Padding="2">
<DockPanel>
<ToggleSwitch Name="_hitRangeToggle" DockPanel.Dock="Right" OnContent="HitTestTextRange" OffContent="HitTestTextPosition" IsCheckedChanged="OnHitTestMethodChanged" />
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="HitTestPoint:" Margin="5,0" />
<TextBlock Name="_coordinates" MinWidth="120" />
<Border Width="5" BorderThickness="1,0,0,0" BorderBrush="Silver" UseLayoutRounding="True" Margin="5,0,0,0" />
<TextBlock Text="TextPosition:" Margin="5,0" />
<TextBlock Name="_hit" MinWidth="60" />
<Border Width="5" BorderThickness="1,0,0,0" BorderBrush="Silver" UseLayoutRounding="True" Margin="5,0,0,0" />
</StackPanel>
</DockPanel>
</Border>
<DockPanel DockPanel.Dock="Top" Margin="5">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
<Label Content="_Font:" Target="{Binding ElementName=_font}" VerticalAlignment="Center" Margin="5,0,0,0" />
<ComboBox Name="_font" ItemsSource="{Binding SystemFonts, Source={x:Static FontManager.Current}}" />
<Label Content="_Size:" Target="{Binding ElementName=_size}" VerticalAlignment="Center" Margin="5,0,0,0" />
<TextBox Name="_size" VerticalAlignment="Center" Text="64" />
<Button VerticalAlignment="Center" Click="OnNewWindowClick" ToolTip.Tip="New window" Margin="5,0,0,0">+</Button>
</StackPanel>
<Label Content="_Text:" Target="{Binding ElementName=_text}" VerticalAlignment="Center"/>
<TextBox Name="_text" Text="Hello!" VerticalAlignment="Center" />
</DockPanel>
<Grid RowDefinitions="*,5,*">
<local:InteractiveLineControl Name="_rendering" DockPanel.Dock="Top" Margin="16" HorizontalAlignment="Center"
Text="{Binding Text, ElementName=_text}"
FontFamily="{Binding SelectedValue, ElementName=_font}"
FontSize="{Binding Text, ElementName=_size}"
Background="BlanchedAlmond"
ExtentStroke="Black"
BaselineStroke="Blue"
TextBoundsStroke="Goldenrod"
RunBoundsStroke="Gold"
NextHitStroke="Green"
PreviousHitStroke="Blue"
BackspaceHitStroke="Red"
DistanceStroke="Black"
PointerMoved="OnPointerMoved"
/>
<GridSplitter Grid.Row="1" />
<TabControl Grid.Row="2" DockPanel.Dock="Bottom" Background="White" BorderBrush="Whitesmoke" BorderThickness="0,1,0,0">
<TabItem Header="Shaped Buffer">
<ListBox Name="_buffer" Grid.IsSharedSizeScope="True" ScrollViewer.HorizontalScrollBarVisibility="Auto" SelectionMode="Multiple" SelectionChanged="OnBufferSelectionChanged" Background="Transparent">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="Background" Value="White" />
</Style>
</ListBox.Styles>
<Border Background="WhiteSmoke" BorderBrush="Silver" BorderThickness="0,1">
<local:GridRow ColumnSpacing="10">
<TextBlock Text="" />
<TextBlock Text="Index" />
<TextBlock Text="Characters" />
<TextBlock Text="Codepoints" />
<TextBlock Text="Glyph" />
<TextBlock Text="Glyph ID" />
<TextBlock Text="Advance" />
<TextBlock Text="Offset" />
<TextBlock Text="Ink Bounds" />
</local:GridRow>
</Border>
</ListBox>
</TabItem>
<TabItem Header="Character Hits">
<ListBox Name="_hits" Grid.IsSharedSizeScope="True" ScrollViewer.HorizontalScrollBarVisibility="Auto" SelectionChanged="OnHitsSelectionChanged" Background="Transparent">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="Background" Value="White" />
</Style>
</ListBox.Styles>
<Border Background="WhiteSmoke" BorderBrush="Silver" BorderThickness="0,1">
<local:GridRow ColumnSpacing="10">
<TextBlock Text="" />
<TextBlock Text="Backspace Hit" />
<TextBlock Text="Previous Hit" />
<TextBlock Text="Index" />
<TextBlock Text="Next Hit" />
<TextBlock Text="Codepoint" />
<TextBlock Text="Character" />
<TextBlock Text="Distance" />
</local:GridRow>
</Border>
</ListBox>
</TabItem>
</TabControl>
</Grid>
</DockPanel>
</Window>

340
samples/TextTestApp/MainWindow.axaml.cs

@ -0,0 +1,340 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
namespace TextTestApp
{
public partial class MainWindow : Window
{
private SelectionAdorner? _selectionAdorner;
public MainWindow()
{
InitializeComponent();
_selectionAdorner = new();
_selectionAdorner.Stroke = Brushes.Red;
_selectionAdorner.Fill = new SolidColorBrush(Colors.LightSkyBlue, 0.25);
_selectionAdorner.IsHitTestVisible = false;
AdornerLayer.SetIsClipEnabled(_selectionAdorner, false);
AdornerLayer.SetAdorner(_rendering, _selectionAdorner);
_rendering.TextLineChanged += OnShapeBufferChanged;
OnShapeBufferChanged();
}
private void OnNewWindowClick(object? sender, RoutedEventArgs e)
{
MainWindow win = new MainWindow();
win.Show();
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.Key == Key.F5)
{
_rendering.InvalidateVisual();
OnShapeBufferChanged();
e.Handled = true;
}
else if (e.Key == Key.Escape)
{
if (_hits.IsKeyboardFocusWithin && _hits.SelectedIndex != -1)
{
_hits.SelectedIndex = -1;
e.Handled = true;
}
else if (_buffer.IsKeyboardFocusWithin && _buffer.SelectedIndex != -1)
{
_buffer.SelectedIndex = -1;
e.Handled = true;
}
}
base.OnKeyDown(e);
}
private void OnShapeBufferChanged(object? sender, EventArgs e) => OnShapeBufferChanged();
private void OnShapeBufferChanged()
{
if (_selectionAdorner == null)
return;
ListBuffers();
ListHits();
Rect bounds = _rendering.LineRenderBounds;
_selectionAdorner!.Transform = Matrix.CreateTranslation(bounds.X, bounds.Y);
}
private void ListBuffers()
{
for (int i = _buffer.ItemCount - 1; i >= 1; i--)
_buffer.Items.RemoveAt(i);
TextLine? textLine = _rendering.TextLine;
if (textLine == null)
return;
double currentX = _rendering.LineRenderBounds.Left;
foreach (TextRun run in textLine.TextRuns)
{
if (run is ShapedTextRun shapedRun)
{
_buffer.Items.Add(new TextBlock
{
Text = $"{run.GetType().Name}: Bidi = {shapedRun.BidiLevel}, Font = {shapedRun.ShapedBuffer.GlyphTypeface.FamilyName}",
FontWeight = FontWeight.Bold,
Padding = new Thickness(10, 0),
Tag = run,
});
ListBuffer(textLine, shapedRun, ref currentX);
}
else
_buffer.Items.Add(new TextBlock
{
Text = run.GetType().Name,
FontWeight = FontWeight.Bold,
Padding = new Thickness(10, 0),
Tag = run
});
}
}
private void ListHits()
{
for (int i = _hits.ItemCount - 1; i >= 1; i--)
_hits.Items.RemoveAt(i);
TextLine? textLine = _rendering.TextLine;
if (textLine == null)
return;
for (int i = 0; i < textLine.Length; i++)
{
string? clusterText = _rendering.Text!.Substring(i, 1);
string? clusterHex = ToHex(clusterText);
var hit = new CharacterHit(i);
var prevHit = textLine.GetPreviousCaretCharacterHit(hit);
var nextHit = textLine.GetNextCaretCharacterHit(hit);
var bkspHit = textLine.GetBackspaceCaretCharacterHit(hit);
GridRow row = new GridRow { ColumnSpacing = 10 };
row.Children.Add(new Control());
row.Children.Add(new TextBlock { Text = $"{bkspHit.FirstCharacterIndex}+{bkspHit.TrailingLength}" });
row.Children.Add(new TextBlock { Text = $"{prevHit.FirstCharacterIndex}+{prevHit.TrailingLength}" });
row.Children.Add(new TextBlock { Text = i.ToString(), FontWeight = FontWeight.Bold });
row.Children.Add(new TextBlock { Text = $"{nextHit.FirstCharacterIndex}+{nextHit.TrailingLength}" });
row.Children.Add(new TextBlock { Text = clusterHex });
row.Children.Add(new TextBlock { Text = clusterText });
row.Children.Add(new TextBlock { Text = textLine.GetDistanceFromCharacterHit(hit).ToString() });
row.Tag = i;
_hits.Items.Add(row);
}
}
private static readonly IBrush TransparentAliceBlue = new SolidColorBrush(0x0F0188FF);
private static readonly IBrush TransparentAntiqueWhite = new SolidColorBrush(0x28DF8000);
private void ListBuffer(TextLine textLine, ShapedTextRun shapedRun, ref double currentX)
{
ShapedBuffer buffer = shapedRun.ShapedBuffer;
int lastClusterStart = -1;
bool oddCluster = false;
IReadOnlyList<GlyphInfo> glyphInfos = buffer;
currentX += shapedRun.GlyphRun.BaselineOrigin.X;
for (var i = 0; i < glyphInfos.Count; i++)
{
GlyphInfo info = glyphInfos[i];
int clusterStart = info.GlyphCluster;
int clusterLength = FindClusterLenghtAt(i);
string? clusterText = _rendering.Text!.Substring(clusterStart, clusterLength);
string? clusterHex = ToHex(clusterText);
Border border = new Border();
if (clusterStart == lastClusterStart)
{
clusterText = clusterHex = null;
}
else
{
oddCluster = !oddCluster;
lastClusterStart = clusterStart;
}
border.Background = oddCluster ? TransparentAliceBlue : TransparentAntiqueWhite;
GridRow row = new GridRow { ColumnSpacing = 10 };
row.Children.Add(new Control());
row.Children.Add(new TextBlock { Text = clusterStart.ToString() });
row.Children.Add(new TextBlock { Text = clusterText });
row.Children.Add(new TextBlock { Text = clusterHex, TextWrapping = TextWrapping.Wrap });
row.Children.Add(new Image { Source = CreateGlyphDrawing(shapedRun.GlyphRun.GlyphTypeface, FontSize, info), Margin = new Thickness(2) });
row.Children.Add(new TextBlock { Text = info.GlyphIndex.ToString() });
row.Children.Add(new TextBlock { Text = info.GlyphAdvance.ToString() });
row.Children.Add(new TextBlock { Text = info.GlyphOffset.ToString() });
Geometry glyph = GetGlyphOutline(shapedRun.GlyphRun.GlyphTypeface, shapedRun.GlyphRun.FontRenderingEmSize, info);
Rect glyphBounds = glyph.Bounds;
Rect offsetBounds = glyphBounds.Translate(new Vector(currentX + info.GlyphOffset.X, info.GlyphOffset.Y));
TextBlock boundsBlock = new TextBlock { Text = offsetBounds.ToString() };
ToolTip.SetTip(boundsBlock, "Origin bounds: " + glyphBounds);
row.Children.Add(boundsBlock);
border.Child = row;
border.Tag = offsetBounds;
_buffer.Items.Add(border);
currentX += glyphInfos[i].GlyphAdvance;
}
int FindClusterLenghtAt(int index)
{
int cluster = glyphInfos[index].GlyphCluster;
if (shapedRun.BidiLevel % 2 == 0)
{
while (++index < glyphInfos.Count)
if (glyphInfos[index].GlyphCluster != cluster)
return glyphInfos[index].GlyphCluster - cluster;
return shapedRun.Length + glyphInfos[0].GlyphCluster - cluster;
}
else
{
while (--index >= 0)
if (glyphInfos[index].GlyphCluster != cluster)
return glyphInfos[index].GlyphCluster - cluster;
return shapedRun.Length + glyphInfos[glyphInfos.Count - 1].GlyphCluster - cluster;
}
}
}
private IImage CreateGlyphDrawing(IGlyphTypeface glyphTypeface, double emSize, GlyphInfo info)
{
return new DrawingImage { Drawing = new GeometryDrawing { Brush = Brushes.Black, Geometry = GetGlyphOutline(glyphTypeface, emSize, info) } };
}
private Geometry GetGlyphOutline(IGlyphTypeface typeface, double emSize, GlyphInfo info)
{
// substitute for GlyphTypeface.GetGlyphOutline
return new GlyphRun(typeface, emSize, new[] { '\0' }, [info]).BuildGeometry();
}
private void OnPointerMoved(object sender, PointerEventArgs e)
{
InteractiveLineControl lineControl = (InteractiveLineControl)sender;
TextLayout textLayout = lineControl.TextLayout;
Rect lineBounds = lineControl.LineRenderBounds;
PointerPoint pointerPoint = e.GetCurrentPoint(lineControl);
Point point = new Point(pointerPoint.Position.X - lineBounds.Left, pointerPoint.Position.Y - lineBounds.Top);
_coordinates.Text = $"{pointerPoint.Position.X:F4}, {pointerPoint.Position.Y:F4}";
TextHitTestResult textHit = textLayout.HitTestPoint(point);
_hit.Text = $"{textHit.TextPosition} ({textHit.CharacterHit.FirstCharacterIndex}+{textHit.CharacterHit.TrailingLength})";
if (textHit.IsTrailing)
_hit.Text += " T";
if (textHit.IsInside)
{
_hits.SelectedIndex = textHit.TextPosition + 1; // header
}
else
_hits.SelectedIndex = -1;
}
private void OnHitTestMethodChanged(object? sender, RoutedEventArgs e)
{
_hits.SelectionMode = _hitRangeToggle.IsChecked == true ? SelectionMode.Multiple : SelectionMode.Single;
}
private void OnHitsSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_selectionAdorner == null)
return;
List<Rect> rectangles = new List<Rect>();
TextLayout textLayout = _rendering.TextLayout;
if (_hitRangeToggle.IsChecked == true)
{
// collect continuous selected indices
List<(int start, int length)> selections = new(1);
int[] indices = _hits.Selection.SelectedIndexes.ToArray();
Array.Sort(indices);
int currentIndex = -1;
int currentLength = 0;
for (int i = 0; i < indices.Length; i++)
if (_hits.Items[indices[i]] is Control { Tag: int index })
{
if (index == currentIndex + currentLength)
{
currentLength++;
}
else
{
if (currentLength > 0)
selections.Add((currentIndex, currentLength));
currentIndex = index;
currentLength = 1;
}
}
if (currentLength > 0)
selections.Add((currentIndex, currentLength));
foreach (var selection in selections)
{
var selectionRectangles = textLayout.HitTestTextRange(selection.start, selection.length);
rectangles.AddRange(selectionRectangles);
}
}
else
{
if (_hits.SelectedItem is Control { Tag: int index })
{
Rect rect = textLayout.HitTestTextPosition(index);
rectangles.Add(rect);
}
}
_selectionAdorner.Rectangles = rectangles;
}
private void OnBufferSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
List<Rect> rectangles = new List<Rect>(_buffer.Selection.Count);
foreach (var row in _buffer.SelectedItems)
if (row is Control { Tag: Rect rect })
rectangles.Add(rect);
_selectionAdorner.Rectangles = rectangles;
}
private static string ToHex(string s)
{
if (string.IsNullOrEmpty(s))
return s;
return string.Join(" ", s.Select(c => ((int)c).ToString("X4")));
}
}
}

25
samples/TextTestApp/Program.cs

@ -0,0 +1,25 @@
using System;
using Avalonia;
namespace TextTestApp
{
static class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
return AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
}
}
}

90
samples/TextTestApp/SelectionAdorner.cs

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
namespace TextTestApp
{
public class SelectionAdorner : Control
{
public static readonly StyledProperty<IBrush?> FillProperty =
AvaloniaProperty.Register<SelectionAdorner, IBrush?>(nameof(Fill));
public static readonly StyledProperty<IBrush?> StrokeProperty =
AvaloniaProperty.Register<SelectionAdorner, IBrush?>(nameof(Stroke));
public static readonly StyledProperty<Matrix> TransformProperty =
AvaloniaProperty.Register<SelectionAdorner, Matrix>(nameof(Transform), Matrix.Identity);
public Matrix Transform
{
get => this.GetValue(TransformProperty);
set => SetValue(TransformProperty, value);
}
public IBrush? Stroke
{
get => GetValue(StrokeProperty);
set => SetValue(StrokeProperty, value);
}
public IBrush? Fill
{
get => GetValue(FillProperty);
set => SetValue(FillProperty, value);
}
private IList<Rect>? _rectangles;
public IList<Rect>? Rectangles
{
get => _rectangles;
set
{
_rectangles = value;
InvalidateVisual();
}
}
public SelectionAdorner()
{
AffectsRender<SelectionAdorner>(FillProperty, StrokeProperty, TransformProperty);
}
public override void Render(DrawingContext context)
{
var rectangles = Rectangles;
if (rectangles == null)
return;
using (context.PushTransform(Transform))
{
Pen pen = new Pen(Stroke, 1);
for (int i = 0; i < rectangles.Count; i++)
{
Rect rectangle = rectangles[i];
Rect normalized = rectangle.Width < 0 ? new Rect(rectangle.TopRight, rectangle.BottomLeft) : rectangle;
if (rectangles[i].Width == 0)
context.DrawLine(pen, rectangle.TopLeft, rectangle.BottomRight);
else
context.DrawRectangle(Fill, pen, normalized);
RenderCue(context, pen, rectangle.TopLeft, 5, isFilled: true);
RenderCue(context, pen, rectangle.TopRight, 5, isFilled: false);
}
}
}
private void RenderCue(DrawingContext context, IPen pen, Point p, double size, bool isFilled)
{
context.DrawGeometry(pen.Brush, pen, new PolylineGeometry(
[
new Point(p.X - size / 2, p.Y - size),
new Point(p.X + size / 2, p.Y - size),
new Point(p.X, p.Y),
new Point(p.X - size / 2, p.Y - size),
], isFilled));
}
}
}

23
samples/TextTestApp/TextTestApp.csproj

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<ApplicationManifest>app.manifest</ApplicationManifest>
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\SourceGenerators.props" />
</Project>

28
samples/TextTestApp/app.manifest

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="ControlCatalog.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows Vista -->
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
<!-- Windows 7 -->
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
<!-- Windows 8 -->
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
<!-- Windows 8.1 -->
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

5
src/Android/Avalonia.Android/AvaloniaActivity.cs

@ -8,10 +8,10 @@ using Android.OS;
using Android.Runtime;
using Android.Views;
using AndroidX.AppCompat.App;
using Avalonia.Platform;
using Avalonia.Android.Platform;
using Avalonia.Android.Platform.Storage;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
namespace Avalonia.Android;
@ -48,6 +48,9 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity
SetContentView(_view);
// By default, the view isn't focused if the activity is created anew, so we force focus.
_view.RequestFocus();
_listener = new GlobalLayoutListener(_view);
_view.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener);

67
src/Android/Avalonia.Android/AvaloniaView.Input.cs

@ -0,0 +1,67 @@
using System;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
namespace Avalonia.Android
{
public partial class AvaloniaView : IInitEditorInfo
{
private Func<TopLevelImpl, EditorInfo, IInputConnection>? _initEditorInfo;
public override IInputConnection OnCreateInputConnection(EditorInfo? outAttrs)
{
return _initEditorInfo?.Invoke(_view, outAttrs!)!;
}
void IInitEditorInfo.InitEditorInfo(Func<TopLevelImpl, EditorInfo, IInputConnection> init)
{
_initEditorInfo = init;
}
protected override void OnFocusChanged(bool gainFocus, FocusSearchDirection direction, global::Android.Graphics.Rect? previouslyFocusedRect)
{
base.OnFocusChanged(gainFocus, direction, previouslyFocusedRect);
_accessHelper?.OnFocusChanged(gainFocus, (int)direction, previouslyFocusedRect);
}
protected override bool DispatchHoverEvent(MotionEvent? e)
{
return _accessHelper?.DispatchHoverEvent(e!) == true || base.DispatchHoverEvent(e);
}
protected override bool DispatchGenericPointerEvent(MotionEvent? e)
{
var result = _view.PointerHelper.DispatchMotionEvent(e, out var callBase);
var baseResult = callBase && base.DispatchGenericPointerEvent(e);
return result ?? baseResult;
}
public override bool DispatchTouchEvent(MotionEvent? e)
{
var result = _view.PointerHelper.DispatchMotionEvent(e, out var callBase);
var baseResult = callBase && base.DispatchTouchEvent(e);
if(result == true)
{
// Request focus for this view
RequestFocus();
}
return result ?? baseResult;
}
public override bool DispatchKeyEvent(KeyEvent? e)
{
var res = _view.KeyboardHelper.DispatchKeyEvent(e, out var callBase);
if (res == false)
callBase = _accessHelper?.DispatchKeyEvent(e!) == false && callBase;
var baseResult = callBase && base.DispatchKeyEvent(e);
return res ?? baseResult;
}
}
}

21
src/Android/Avalonia.Android/AvaloniaView.cs

@ -17,7 +17,7 @@ using Avalonia.Rendering;
namespace Avalonia.Android
{
public class AvaloniaView : FrameLayout
public partial class AvaloniaView : FrameLayout
{
private EmbeddableControlRoot? _root;
private ExploreByTouchHelper? _accessHelper;
@ -102,24 +102,6 @@ namespace Avalonia.Android
base.OnAttachedToWindow();
}
protected override void OnFocusChanged(bool gainFocus, FocusSearchDirection direction, global::Android.Graphics.Rect? previouslyFocusedRect)
{
base.OnFocusChanged(gainFocus, direction, previouslyFocusedRect);
_accessHelper?.OnFocusChanged(gainFocus, (int)direction, previouslyFocusedRect);
}
protected override bool DispatchHoverEvent(MotionEvent? e)
{
return _accessHelper?.DispatchHoverEvent(e!) == true || base.DispatchHoverEvent(e);
}
public override bool DispatchKeyEvent(KeyEvent? e)
{
if (!_view.View.DispatchKeyEvent(e))
return _accessHelper?.DispatchKeyEvent(e!) == true || base.DispatchKeyEvent(e);
return true;
}
[SupportedOSPlatform("android24.0")]
public override void OnVisibilityAggregated(bool isVisible)
{
@ -182,7 +164,6 @@ namespace Avalonia.Android
{
public ViewImpl(AvaloniaView avaloniaView) : base(avaloniaView)
{
View.Focusable = true;
View.FocusChange += ViewImpl_FocusChange;
}

48
src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs

@ -2,6 +2,8 @@
using Android.Content;
using Android.Content.Res;
using Android.Provider;
using Android.Views;
using Avalonia.Input;
using Avalonia.Platform;
using Color = Avalonia.Media.Color;
@ -11,10 +13,18 @@ namespace Avalonia.Android.Platform;
internal class AndroidPlatformSettings : DefaultPlatformSettings
{
private PlatformColorValues _latestValues;
private TimeSpan _holdWaitDuration = TimeSpan.FromMilliseconds(300);
private TimeSpan _doubleTapTime = TimeSpan.FromMilliseconds(500);
private Size _doubleTapSize = new Size(16,16);
private Size _tapSize = new Size(10,10);
public AndroidPlatformSettings()
{
_latestValues = base.GetColorValues();
if (global::Android.App.Application.Context is { } context)
{
GetInputConfigValues(context);
}
}
public override PlatformColorValues GetColorValues()
@ -22,6 +32,23 @@ internal class AndroidPlatformSettings : DefaultPlatformSettings
return _latestValues;
}
public override TimeSpan GetDoubleTapTime(PointerType type)
{
return type == PointerType.Mouse ? base.GetDoubleTapTime(type) : _doubleTapTime;
}
public override Size GetDoubleTapSize(PointerType type)
{
return type == PointerType.Mouse ? base.GetDoubleTapSize(type) : _doubleTapSize;
}
public override Size GetTapSize(PointerType type)
{
return type == PointerType.Mouse ? base.GetTapSize(type) : _tapSize;
}
public override TimeSpan HoldWaitDuration => _holdWaitDuration;
internal void OnViewConfigurationChanged(Context context, Configuration configuration)
{
if (context.Resources is null)
@ -80,9 +107,30 @@ internal class AndroidPlatformSettings : DefaultPlatformSettings
_latestValues = _latestValues with { ThemeVariant = systemTheme };
}
GetInputConfigValues(context);
OnColorValuesChanged(_latestValues);
}
private void GetInputConfigValues(Context context)
{
_holdWaitDuration = TimeSpan.FromMilliseconds(ViewConfiguration.LongPressTimeout);
if (OperatingSystem.IsAndroidVersionAtLeast(31))
{
_doubleTapTime = TimeSpan.FromMilliseconds(ViewConfiguration.MultiPressTimeout);
}
var config = ViewConfiguration.Get(context);
var scaling = context.Resources?.DisplayMetrics?.Density ?? 1;
if (config != null)
{
var size = config.ScaledDoubleTapSlop * 2 / scaling;
_doubleTapSize = new Size(size, size);
size = config.ScaledTouchSlop * 2 / scaling;
_tapSize = new Size(size, size);
}
}
private static ColorContrastPreference IsHighContrast(Context context)
{
try

3
src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs

@ -44,9 +44,6 @@ namespace Avalonia.Android.Platform.Input
public AndroidInputMethod(TView host)
{
if (host.OnCheckIsTextEditor() == false)
throw new InvalidOperationException("Host should return true from OnCheckIsTextEditor()");
_host = host;
_imm = host.Context?.GetSystemService(Context.InputMethodService).JavaCast<InputMethodManager>()
?? throw new InvalidOperationException("Context.InputMethodService is expected to be not null.");

59
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -5,9 +5,7 @@ using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using AndroidX.AppCompat.App;
using AndroidX.Core.View;
using Avalonia.Android.Platform.Input;
@ -16,13 +14,11 @@ using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Android.Platform.Storage;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering.Composition;
@ -35,7 +31,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
private readonly AndroidMotionEventsHelper _pointerHelper;
private readonly AndroidInputMethod<ViewImpl> _textInputMethod;
private readonly AndroidInputMethod<AvaloniaView> _textInputMethod;
private readonly INativeControlHostImpl _nativeControlHost;
private readonly IStorageProvider? _storageProvider;
private readonly AndroidSystemNavigationManagerImpl _systemNavigationManager;
@ -43,7 +39,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly ClipboardImpl _clipboard;
private readonly AndroidLauncher? _launcher;
private readonly AndroidScreens? _screens;
private ViewImpl _view;
private SurfaceViewImpl _view;
private WindowTransparencyLevel _transparencyLevel;
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@ -53,8 +49,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
throw new ArgumentException("AvaloniaView.Context must not be null");
}
_view = new ViewImpl(avaloniaView.Context, this, placeOnTop);
_textInputMethod = new AndroidInputMethod<ViewImpl>(_view);
_view = new SurfaceViewImpl(avaloniaView.Context, this, placeOnTop);
_textInputMethod = new AndroidInputMethod<AvaloniaView>(avaloniaView);
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
_pointerHelper = new AndroidMotionEventsHelper(this);
_clipboard = new ClipboardImpl(avaloniaView.Context.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>());
@ -142,13 +138,13 @@ namespace Avalonia.Android.Platform.SkiaPlatform
Resized?.Invoke(size, WindowResizeReason.Layout);
}
sealed class ViewImpl : InvalidationAwareSurfaceView, IInitEditorInfo
sealed class SurfaceViewImpl : InvalidationAwareSurfaceView
{
private readonly TopLevelImpl _tl;
private Size _oldSize;
private double _oldScaling;
public ViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context)
public SurfaceViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context)
{
_tl = tl;
if (placeOnTop)
@ -177,30 +173,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
base.DispatchDraw(canvas);
}
protected override bool DispatchGenericPointerEvent(MotionEvent? e)
{
var result = _tl._pointerHelper.DispatchMotionEvent(e, out var callBase);
var baseResult = callBase && base.DispatchGenericPointerEvent(e);
return result ?? baseResult;
}
public override bool DispatchTouchEvent(MotionEvent? e)
{
var result = _tl._pointerHelper.DispatchMotionEvent(e, out var callBase);
var baseResult = callBase && base.DispatchTouchEvent(e);
return result ?? baseResult;
}
public override bool DispatchKeyEvent(KeyEvent? e)
{
var res = _tl._keyboardHelper.DispatchKeyEvent(e, out var callBase);
var baseResult = callBase && base.DispatchKeyEvent(e);
return res ?? baseResult;
}
public override void SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
{
base.SurfaceChanged(holder, format, width, height);
@ -233,23 +205,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_tl.Compositor.RequestCompositionUpdate(drawingFinished.Run);
base.SurfaceRedrawNeededAsync(holder, drawingFinished);
}
public override bool OnCheckIsTextEditor()
{
return true;
}
private Func<TopLevelImpl, EditorInfo, IInputConnection>? _initEditorInfo;
public void InitEditorInfo(Func<TopLevelImpl, EditorInfo, IInputConnection> init)
{
_initEditorInfo = init;
}
public override IInputConnection OnCreateInputConnection(EditorInfo? outAttrs)
{
return _initEditorInfo?.Invoke(_tl, outAttrs!)!;
}
}
public IPopupImpl? CreatePopup() => null;
@ -309,6 +264,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
double EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Scaling => _view.Scaling;
internal AndroidInsetsManager? InsetsManager => _insetsManager;
internal AndroidKeyboardEventsHelper<TopLevelImpl> KeyboardHelper => _keyboardHelper;
internal AndroidMotionEventsHelper PointerHelper => _pointerHelper;
public void SetTransparencyLevelHint(IReadOnlyList<WindowTransparencyLevel> transparencyLevels)
{

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

@ -566,6 +566,8 @@ namespace Avalonia.Input
{
FocusManager.GetFocusManager(this)?.ClearFocusOnElementRemoved(this, e.Parent);
}
IsKeyboardFocusWithin = false;
}
/// <summary>

4
src/Avalonia.Base/Media/CharacterHit.cs

@ -35,6 +35,10 @@ namespace Avalonia.Media
/// <summary>
/// Gets the trailing length value for the character that got hit.
/// </summary>
/// <remarks>
/// In the case of a leading edge, this value is 0. In the case of a trailing edge,
/// this value is the number of code points until the next valid caret position.
/// </remarks>
public int TrailingLength { get; }
public bool Equals(CharacterHit other)

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

@ -1,7 +1,7 @@
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Generic implementation of TextParagraphProperties
/// Generic implementation of <see cref="TextParagraphProperties"/>.
/// </summary>
public sealed class GenericTextParagraphProperties : TextParagraphProperties
{
@ -11,45 +11,45 @@
private double _lineHeight;
/// <summary>
/// Constructing TextParagraphProperties
/// Initializes a new instance of the <see cref="GenericTextParagraphProperties"/>.
/// </summary>
/// <param name="defaultTextRunProperties">default paragraph's default run properties</param>
/// <param name="textAlignment">logical horizontal alignment</param>
/// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param>
/// <param name="letterSpacing">letter spacing</param>
/// <param name="defaultTextRunProperties">Default text run properties, such as typeface or foreground brush.</param>
/// <param name="textAlignment">The alignment of inline content in a block.</param>
/// <param name="textWrapping">A value that controls whether text wraps when it reaches the flow edge of its containing block box.</param>
/// <param name="lineHeight">Paragraph's line spacing.</param>
/// <param name="letterSpacing">The amount of letter spacing.</param>
public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties,
TextAlignment textAlignment = TextAlignment.Left,
TextWrapping textWrap = TextWrapping.NoWrap,
TextWrapping textWrapping = TextWrapping.NoWrap,
double lineHeight = 0,
double letterSpacing = 0)
{
DefaultTextRunProperties = defaultTextRunProperties;
_textAlignment = textAlignment;
_textWrap = textWrap;
_textWrap = textWrapping;
_lineHeight = lineHeight;
LetterSpacing = letterSpacing;
}
/// <summary>
/// Constructing TextParagraphProperties
/// Initializes a new instance of the <see cref="GenericTextParagraphProperties"/>.
/// </summary>
/// <param name="flowDirection">text flow direction</param>
/// <param name="textAlignment">logical horizontal alignment</param>
/// <param name="firstLineInParagraph">true if the paragraph is the first line in the paragraph</param>
/// <param name="alwaysCollapsible">true if the line is always collapsible</param>
/// <param name="defaultTextRunProperties">default paragraph's default run properties</param>
/// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param>
/// <param name="indent">line indentation</param>
/// <param name="letterSpacing">letter spacing</param>
/// <param name="flowDirection">The primary text advance direction.</param>
/// <param name="textAlignment">The alignment of inline content in a block.</param>
/// <param name="firstLineInParagraph"><see langword="true"/> if the paragraph is the first line in the paragraph</param>
/// <param name="alwaysCollapsible"><see langword="true"/> if the formatted line may always be collapsed. If <see langword="false"/> (the default), only lines that overflow the paragraph width are collapsed.</param>
/// <param name="defaultTextRunProperties">Default text run properties, such as typeface or foreground brush.</param>
/// <param name="textWrapping">A value that controls whether text wraps when it reaches the flow edge of its containing block box.</param>
/// <param name="lineHeight">Paragraph's line spacing.</param>
/// <param name="indent">The amount of line indentation.</param>
/// <param name="letterSpacing">The amount of letter spacing.</param>
public GenericTextParagraphProperties(
FlowDirection flowDirection,
TextAlignment textAlignment,
bool firstLineInParagraph,
bool alwaysCollapsible,
TextRunProperties defaultTextRunProperties,
TextWrapping textWrap,
TextWrapping textWrapping,
double lineHeight,
double indent,
double letterSpacing)
@ -59,16 +59,16 @@
FirstLineInParagraph = firstLineInParagraph;
AlwaysCollapsible = alwaysCollapsible;
DefaultTextRunProperties = defaultTextRunProperties;
_textWrap = textWrap;
_textWrap = textWrapping;
_lineHeight = lineHeight;
LetterSpacing = letterSpacing;
Indent = indent;
}
/// <summary>
/// Constructing TextParagraphProperties from another one
/// Initializes a new instance of the <see cref="GenericTextParagraphProperties"/> with values copied from the specified <see cref="TextParagraphProperties"/>.
/// </summary>
/// <param name="textParagraphProperties">source line props</param>
/// <param name="textParagraphProperties">The <see cref="TextParagraphProperties"/> to copy values from.</param>
public GenericTextParagraphProperties(TextParagraphProperties textParagraphProperties)
: this(textParagraphProperties.FlowDirection,
textParagraphProperties.TextAlignment,
@ -82,64 +82,43 @@
{
}
/// <summary>
/// This property specifies whether the primary text advance
/// direction shall be left-to-right, right-to-left, or top-to-bottom.
/// </summary>
/// <inheritdoc/>
public override FlowDirection FlowDirection
{
get { return _flowDirection; }
}
/// <summary>
/// This property describes how inline content of a block is aligned.
/// </summary>
/// <inheritdoc/>
public override TextAlignment TextAlignment
{
get { return _textAlignment; }
}
/// <summary>
/// Paragraph's line height
/// </summary>
/// <inheritdoc/>
public override double LineHeight
{
get { return _lineHeight; }
}
/// <summary>
/// Indicates the first line of the paragraph.
/// </summary>
/// <inheritdoc/>
public override bool FirstLineInParagraph { get; }
/// <summary>
/// If true, the formatted line may always be collapsed. If false (the default),
/// only lines that overflow the paragraph width are collapsed.
/// </summary>
/// <inheritdoc/>
public override bool AlwaysCollapsible { get; }
/// <summary>
/// Paragraph's default run properties
/// </summary>
/// <inheritdoc/>
public override TextRunProperties DefaultTextRunProperties { get; }
/// <summary>
/// This property controls whether or not text wraps when it reaches the flow edge
/// of its containing block box
/// </summary>
/// <inheritdoc/>
public override TextWrapping TextWrapping
{
get { return _textWrap; }
}
/// <summary>
/// Line indentation
/// </summary>
/// <inheritdoc/>
public override double Indent { get; }
/// <summary>
/// The letter spacing
/// </summary>
/// <inheritdoc/>
public override double LetterSpacing { get; }
/// <summary>

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

@ -6,63 +6,68 @@
public abstract class TextParagraphProperties
{
/// <summary>
/// This property specifies whether the primary text advance
/// direction shall be left-to-right, right-to-left.
/// Gets a value that specifies whether the primary text advance direction shall be left-to-right, or right-to-left.
/// </summary>
public abstract FlowDirection FlowDirection { get; }
/// <summary>
/// Gets the text alignment.
/// Gets a value that describes how an inline content of a block is aligned.
/// </summary>
public abstract TextAlignment TextAlignment { get; }
/// <summary>
/// Paragraph's line height
/// Gets the height of a line of text.
/// </summary>
public abstract double LineHeight { get; }
/// <summary>
/// Paragraph's line spacing
/// Gets or sets paragraph's line spacing.
/// </summary>
internal double LineSpacing { get; set; }
/// <summary>
/// Indicates the first line of the paragraph.
/// Gets a value that indicates whether the text run is the first line of the paragraph.
/// </summary>
public abstract bool FirstLineInParagraph { get; }
/// <summary>
/// Gets a value that indicates whether a formatted line can always be collapsed.
/// </summary>
/// <remarks>
/// If true, the formatted line may always be collapsed. If false (the default),
/// only lines that overflow the paragraph width are collapsed.
/// </summary>
/// </remarks>
public virtual bool AlwaysCollapsible
{
get { return false; }
}
/// <summary>
/// Gets the default text style.
/// Gets the default text run properties, such as typeface or foreground brush.
/// </summary>
public abstract TextRunProperties DefaultTextRunProperties { get; }
/// <summary>
/// Gets the collection of TextDecoration objects.
/// </summary>
/// <remarks>
/// If not null, text decorations to apply to all runs in the line. This is in addition
/// to any text decorations specified by the TextRunProperties for individual text runs.
/// </summary>
public virtual TextDecorationCollection? TextDecorations => null;
/// <summary>
/// Gets the text wrapping.
/// Gets a value that controls whether text wraps when it reaches the flow edge of its containing block box.
/// </summary>
public abstract TextWrapping TextWrapping { get; }
/// <summary>
/// Line indentation
/// Gets the amount of line indentation.
/// </summary>
public abstract double Indent { get; }
/// <summary>
/// Get the paragraph indentation.
/// Gets the paragraph indentation.
/// </summary>
public virtual double ParagraphIndent
{
@ -75,7 +80,7 @@
public virtual double DefaultIncrementalTab => 0;
/// <summary>
/// Gets the letter spacing.
/// Gets the amount of letter spacing.
/// </summary>
public virtual double LetterSpacing { get; }
}

66
src/Avalonia.Base/Threading/Dispatcher.Invoke.cs

@ -672,4 +672,70 @@ public partial class Dispatcher
/// </summary>
public DispatcherPriorityAwaitable<T> AwaitWithPriority<T>(Task<T> task, DispatcherPriority priority) =>
new(this, task, priority);
/// <summary>
/// Creates an awaitable object that asynchronously resumes execution on the dispatcher.
/// </summary>
/// <returns>
/// An awaitable object that asynchronously resumes execution on the dispatcher.
/// </returns>
/// <remarks>
/// This method is equivalent to calling the <see cref="Resume(DispatcherPriority)"/> method
/// and passing in <see cref="DispatcherPriority.Background"/>.
/// </remarks>
public DispatcherPriorityAwaitable Resume() =>
Resume(DispatcherPriority.Background);
/// <summary>``
/// Creates an awaitable object that asynchronously resumes execution on the dispatcher. The work that occurs
/// when control returns to the code awaiting the result of this method is scheduled with the specified priority.
/// </summary>
/// <param name="priority">The priority at which to schedule the continuation.</param>
/// <returns>
/// An awaitable object that asynchronously resumes execution on the dispatcher.
/// </returns>
public DispatcherPriorityAwaitable Resume(DispatcherPriority priority)
{
DispatcherPriority.Validate(priority, nameof(priority));
return new(this, null, priority);
}
/// <summary>
/// Creates an awaitable object that asynchronously yields control back to the current dispatcher
/// and provides an opportunity for the dispatcher to process other events.
/// </summary>
/// <returns>
/// An awaitable object that asynchronously yields control back to the current dispatcher
/// and provides an opportunity for the dispatcher to process other events.
/// </returns>
/// <remarks>
/// This method is equivalent to calling the <see cref="Yield(DispatcherPriority)"/> method
/// and passing in <see cref="DispatcherPriority.Background"/>.
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The current thread is not the UI thread.
/// </exception>
public static DispatcherPriorityAwaitable Yield() =>
Yield(DispatcherPriority.Background);
/// <summary>
/// Creates an cawaitable object that asynchronously yields control back to the current dispatcher
/// and provides an opportunity for the dispatcher to process other events. The work that occurs when
/// control returns to the code awaiting the result of this method is scheduled with the specified priority.
/// </summary>
/// <param name="priority">The priority at which to schedule the continuation.</param>
/// <returns>
/// An awaitable object that asynchronously yields control back to the current dispatcher
/// and provides an opportunity for the dispatcher to process other events.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The current thread is not the UI thread.
/// </exception>
public static DispatcherPriorityAwaitable Yield(DispatcherPriority priority)
{
// TODO12: Update to use Dispatcher.CurrentDispatcher once multi-dispatcher support is merged
var current = UIThread;
current.VerifyAccess();
return UIThread.Resume(priority);
}
}

122
src/Avalonia.Base/Threading/DispatcherPriorityAwaitable.cs

@ -1,40 +1,130 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Threading;
public class DispatcherPriorityAwaitable : INotifyCompletion
/// <summary>
/// A simple awaitable type that will return a DispatcherPriorityAwaiter.
/// </summary>
public struct DispatcherPriorityAwaitable
{
private readonly Dispatcher _dispatcher;
private protected readonly Task Task;
private readonly Task? _task;
private readonly DispatcherPriority _priority;
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task task, DispatcherPriority priority)
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task? task, DispatcherPriority priority)
{
_dispatcher = dispatcher;
Task = task;
_task = task;
_priority = priority;
}
public void OnCompleted(Action continuation) =>
Task.ContinueWith(_ => _dispatcher.Post(continuation, _priority));
public bool IsCompleted => Task.IsCompleted;
public DispatcherPriorityAwaiter GetAwaiter() => new(_dispatcher, _task, _priority);
}
/// <summary>
/// A simple awaiter type that will queue the continuation to a dispatcher at a specific priority.
/// </summary>
/// <remarks>
/// This is returned from DispatcherPriorityAwaitable.GetAwaiter()
/// </remarks>
public struct DispatcherPriorityAwaiter : INotifyCompletion
{
private readonly Dispatcher _dispatcher;
private readonly Task? _task;
private readonly DispatcherPriority _priority;
internal DispatcherPriorityAwaiter(Dispatcher dispatcher, Task? task, DispatcherPriority priority)
{
_dispatcher = dispatcher;
_task = task;
_priority = priority;
}
public void OnCompleted(Action continuation)
{
if(_task == null || _task.IsCompleted)
_dispatcher.Post(continuation, _priority);
else
{
var self = this;
_task.ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
{
self._dispatcher.Post(continuation, self._priority);
});
}
}
/// <summary>
/// This always returns false since continuation is requested to be queued to a dispatcher queue
/// </summary>
public bool IsCompleted => false;
public void GetResult()
{
if (_task != null)
_task.GetAwaiter().GetResult();
}
}
/// <summary>
/// A simple awaitable type that will return a DispatcherPriorityAwaiter&lt;T&gt;.
/// </summary>
public struct DispatcherPriorityAwaitable<T>
{
private readonly Dispatcher _dispatcher;
private readonly Task<T> _task;
private readonly DispatcherPriority _priority;
public void GetResult() => Task.GetAwaiter().GetResult();
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority)
{
_dispatcher = dispatcher;
_task = task;
_priority = priority;
}
public DispatcherPriorityAwaitable GetAwaiter() => this;
public DispatcherPriorityAwaiter<T> GetAwaiter() => new(_dispatcher, _task, _priority);
}
public sealed class DispatcherPriorityAwaitable<T> : DispatcherPriorityAwaitable
/// <summary>
/// A simple awaiter type that will queue the continuation to a dispatcher at a specific priority.
/// </summary>
/// <remarks>
/// This is returned from DispatcherPriorityAwaitable&lt;T&gt;.GetAwaiter()
/// </remarks>
public struct DispatcherPriorityAwaiter<T> : INotifyCompletion
{
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority) : base(
dispatcher, task, priority)
private readonly Dispatcher _dispatcher;
private readonly Task<T> _task;
private readonly DispatcherPriority _priority;
internal DispatcherPriorityAwaiter(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority)
{
_dispatcher = dispatcher;
_task = task;
_priority = priority;
}
public new T GetResult() => ((Task<T>)Task).GetAwaiter().GetResult();
public void OnCompleted(Action continuation)
{
if(_task.IsCompleted)
_dispatcher.Post(continuation, _priority);
else
{
var self = this;
_task.ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
{
self._dispatcher.Post(continuation, self._priority);
});
}
}
/// <summary>
/// This always returns false since continuation is requested to be queued to a dispatcher queue
/// </summary>
public bool IsCompleted => false;
public new DispatcherPriorityAwaitable<T> GetAwaiter() => this;
}
public void GetResult() => _task.GetAwaiter().GetResult();
}

111
src/Avalonia.Base/Utilities/WeakEvent.cs

@ -1,5 +1,7 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using Avalonia.Collections.Pooled;
using Avalonia.Threading;
namespace Avalonia.Utilities;
@ -10,8 +12,8 @@ namespace Avalonia.Utilities;
public sealed class WeakEvent<TSender, TEventArgs> : WeakEvent where TSender : class
{
private readonly Func<TSender, EventHandler<TEventArgs>, Action> _subscribe;
private readonly ConditionalWeakTable<object, Subscription> _subscriptions = new();
private readonly ConditionalWeakTable<TSender, Subscription> _subscriptions = new();
private readonly ConditionalWeakTable<TSender, Subscription>.CreateValueCallback _createSubscription;
internal WeakEvent(
Action<TSender, EventHandler<TEventArgs>> subscribe,
@ -22,33 +24,43 @@ public sealed class WeakEvent<TSender, TEventArgs> : WeakEvent where TSender : c
subscribe(t, s);
return () => unsubscribe(t, s);
};
_createSubscription = CreateSubscription;
}
internal WeakEvent(Func<TSender, EventHandler<TEventArgs>, Action> subscribe)
{
_subscribe = subscribe;
_createSubscription = CreateSubscription;
}
public void Subscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
{
if (!_subscriptions.TryGetValue(target, out var subscription))
_subscriptions.Add(target, subscription = new Subscription(this, target));
subscription.Add(subscriber);
var spinWait = default(SpinWait);
while (true)
{
var subscription = _subscriptions.GetValue(target, _createSubscription);
if (subscription.Add(subscriber))
break;
spinWait.SpinOnce();
}
}
public void Unsubscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
{
if (_subscriptions.TryGetValue(target, out var subscription))
if (_subscriptions.TryGetValue(target, out var subscription))
subscription.Remove(subscriber);
}
private Subscription CreateSubscription(TSender key) => new(this, key);
private sealed class Subscription
{
private readonly WeakEvent<TSender, TEventArgs> _ev;
private readonly TSender _target;
private readonly Action _compact;
private readonly Action _unsubscribe;
private readonly WeakHashList<IWeakEventSubscriber<TEventArgs>> _list = new();
private readonly object _lock = new();
private Action? _unsubscribe;
private bool _compactScheduled;
private bool _destroyed;
@ -57,7 +69,6 @@ public sealed class WeakEvent<TSender, TEventArgs> : WeakEvent where TSender : c
_ev = ev;
_target = target;
_compact = Compact;
_unsubscribe = ev._subscribe(target, OnEvent);
}
private void Destroy()
@ -65,19 +76,42 @@ public sealed class WeakEvent<TSender, TEventArgs> : WeakEvent where TSender : c
if(_destroyed)
return;
_destroyed = true;
_unsubscribe();
_unsubscribe?.Invoke();
_ev._subscriptions.Remove(_target);
}
public void Add(IWeakEventSubscriber<TEventArgs> s) => _list.Add(s);
public bool Add(IWeakEventSubscriber<TEventArgs> s)
{
if (_destroyed)
return false;
lock (_lock)
{
if (_destroyed)
return false;
_unsubscribe ??= _ev._subscribe(_target, OnEvent);
_list.Add(s);
return true;
}
}
public void Remove(IWeakEventSubscriber<TEventArgs> s)
{
_list.Remove(s);
if(_list.IsEmpty)
Destroy();
else if(_list.NeedCompact && _compactScheduled)
ScheduleCompact();
if (_destroyed)
return;
lock (_lock)
{
if (_destroyed)
return;
_list.Remove(s);
if(_list.IsEmpty)
Destroy();
else if(_list.NeedCompact && _compactScheduled)
ScheduleCompact();
}
}
private void ScheduleCompact()
@ -90,23 +124,40 @@ public sealed class WeakEvent<TSender, TEventArgs> : WeakEvent where TSender : c
private void Compact()
{
if(!_compactScheduled)
if (_destroyed)
return;
_compactScheduled = false;
_list.Compact();
if (_list.IsEmpty)
Destroy();
lock (_lock)
{
if (_destroyed)
return;
if(!_compactScheduled)
return;
_compactScheduled = false;
_list.Compact();
if (_list.IsEmpty)
Destroy();
}
}
private void OnEvent(object? sender, TEventArgs eventArgs)
{
var alive = _list.GetAlive();
if(alive == null)
Destroy();
else
PooledList<IWeakEventSubscriber<TEventArgs>>? alive;
lock (_lock)
{
alive = _list.GetAlive();
if (alive == null)
{
Destroy();
return;
}
}
foreach(var item in alive.Span)
item.OnEvent(_target, _ev, eventArgs);
lock (_lock)
{
foreach(var item in alive.Span)
item.OnEvent(_target, _ev, eventArgs);
WeakHashList<IWeakEventSubscriber<TEventArgs>>.ReturnToSharedPool(alive);
if(_list.NeedCompact && !_compactScheduled)
ScheduleCompact();
@ -124,13 +175,13 @@ public class WeakEvent
{
return new WeakEvent<TSender, TEventArgs>(subscribe, unsubscribe);
}
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>(
Func<TSender, EventHandler<TEventArgs>, Action> subscribe) where TSender : class where TEventArgs : EventArgs
{
return new WeakEvent<TSender, TEventArgs>(subscribe);
}
public static WeakEvent<TSender, EventArgs> Register<TSender>(
Action<TSender, EventHandler> subscribe,
Action<TSender, EventHandler> unsubscribe) where TSender : class

23
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs

@ -22,7 +22,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty<int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<AutoCompleteBox>(new(
defaultValue: 0,
defaultBindingMode:BindingMode.TwoWay));
defaultBindingMode: BindingMode.TwoWay));
public static readonly StyledProperty<string?> WatermarkProperty =
TextBox.WatermarkProperty.AddOwner<AutoCompleteBox>();
@ -72,6 +72,12 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<AutoCompleteBox, IDataTemplate>(
nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="ClearSelectionOnLostFocus"/> property
/// </summary>
public static readonly StyledProperty<bool> ClearSelectionOnLostFocusProperty =
TextBox.ClearSelectionOnLostFocusProperty.AddOwner<AutoCompleteBox>();
/// <summary>
/// Identifies the <see cref="IsDropDownOpen" /> property.
/// </summary>
@ -295,6 +301,15 @@ namespace Avalonia.Controls
set => SetValue(IsDropDownOpenProperty, value);
}
/// <summary>
/// Gets or sets a value that determines whether the <see cref="AutoCompleteBox"/> clears its selection after it loses focus.
/// </summary>
public bool ClearSelectionOnLostFocus
{
get => GetValue(ClearSelectionOnLostFocusProperty);
set => SetValue(ClearSelectionOnLostFocusProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="T:Avalonia.Data.Binding" /> that
/// is used to get the values for display in the text portion of
@ -484,7 +499,7 @@ namespace Avalonia.Controls
get => GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
/// <summary>
/// Gets or sets the maximum number of characters that the <see cref="AutoCompleteBox"/> can accept.
/// This constraint only applies for manually entered (user-inputted) text.
@ -494,7 +509,7 @@ namespace Avalonia.Controls
get => GetValue(MaxLengthProperty);
set => SetValue(MaxLengthProperty, value);
}
/// <summary>
/// Gets or sets custom content that is positioned on the left side of the text layout box
/// </summary>
@ -511,6 +526,6 @@ namespace Avalonia.Controls
{
get => GetValue(InnerRightContentProperty);
set => SetValue(InnerRightContentProperty, value);
}
}
}
}

5
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs

@ -813,7 +813,10 @@ namespace Avalonia.Controls
_userCalledPopulate = false;
if (ContextMenu is not { IsOpen: true })
var textBoxContextMenuIsOpen = TextBox?.ContextFlyout?.IsOpen == true || TextBox?.ContextMenu?.IsOpen == true;
var contextMenuIsOpen = ContextFlyout?.IsOpen == true || ContextMenu?.IsOpen == true;
if (!textBoxContextMenuIsOpen && !contextMenuIsOpen && ClearSelectionOnLostFocus)
{
ClearTextBoxSelection();
}

135
src/Avalonia.Controls/Calendar/Calendar.cs

@ -237,6 +237,8 @@ namespace Avalonia.Controls
private bool _isShiftPressed;
private bool _displayDateIsChanging;
private bool _isTapRangeSelectionActive;
private DateTime? _tapRangeStart;
internal CalendarDayButton? FocusButton { get; set; }
internal CalendarButton? FocusCalendarButton { get; set; }
@ -437,6 +439,11 @@ namespace Avalonia.Controls
nameof(SelectionMode),
defaultValue: CalendarSelectionMode.SingleDate);
public static readonly StyledProperty<bool> AllowTapRangeSelectionProperty =
AvaloniaProperty.Register<Calendar, bool>(
nameof(AllowTapRangeSelection),
defaultValue: true);
/// <summary>
/// Gets or sets a value that indicates what kind of selections are
/// allowed.
@ -462,6 +469,24 @@ namespace Avalonia.Controls
set => SetValue(SelectionModeProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether tap-to-select range mode is enabled.
/// When enabled, users can tap a start date and then tap an end date to select a range.
/// </summary>
/// <value>
/// True to enable tap range selection; otherwise, false. The default is false.
/// </value>
/// <remarks>
/// This feature only works when SelectionMode is set to SingleRange.
/// When enabled, the first tap selects the start date, and the second tap selects
/// the end date to complete the range. Tapping a third date starts a new range.
/// </remarks>
public bool AllowTapRangeSelection
{
get => GetValue(AllowTapRangeSelectionProperty);
set => SetValue(AllowTapRangeSelectionProperty, value);
}
private void OnSelectionModeChanged(AvaloniaPropertyChangedEventArgs e)
{
if (IsValidSelectionMode(e.NewValue!))
@ -470,6 +495,10 @@ namespace Avalonia.Controls
SetCurrentValue(SelectedDateProperty, null);
_displayDateIsChanging = false;
SelectedDates.Clear();
// Reset tap range selection state when mode changes
_isTapRangeSelectionActive = false;
_tapRangeStart = null;
}
else
{
@ -477,6 +506,12 @@ namespace Avalonia.Controls
}
}
private void OnAllowTapRangeSelectionChanged(AvaloniaPropertyChangedEventArgs e)
{
_isTapRangeSelectionActive = false;
_tapRangeStart = null;
}
/// <summary>
/// Inherited code: Requires comment.
/// </summary>
@ -1450,6 +1485,94 @@ namespace Avalonia.Controls
SelectedDates.AddRange(HoverStart.Value, HoverEnd.Value);
}
}
/// <summary>
/// Handles tap range selection logic for date range selection.
/// </summary>
/// <param name="selectedDate">The date that was tapped.</param>
/// <returns>True if the tap was handled as part of range selection; otherwise, false.</returns>
internal bool ProcessTapRangeSelection(DateTime selectedDate)
{
if (!AllowTapRangeSelection ||
(SelectionMode != CalendarSelectionMode.SingleRange && SelectionMode != CalendarSelectionMode.MultipleRange))
{
return false;
}
if (!IsValidDateSelection(this, selectedDate))
{
return false;
}
if (!_isTapRangeSelectionActive || !_tapRangeStart.HasValue)
{
_isTapRangeSelectionActive = true;
_tapRangeStart = selectedDate;
if (SelectionMode == CalendarSelectionMode.SingleRange)
{
foreach (DateTime item in SelectedDates)
{
RemovedItems.Add(item);
}
SelectedDates.ClearInternal();
}
if (!SelectedDates.Contains(selectedDate))
{
SelectedDates.Add(selectedDate);
}
return true;
}
else
{
DateTime startDate = _tapRangeStart.Value;
DateTime endDate = selectedDate;
if (DateTime.Compare(startDate, endDate) > 0)
{
(startDate, endDate) = (endDate, startDate);
}
CalendarDateRange range = new CalendarDateRange(startDate, endDate);
if (BlackoutDates.ContainsAny(range))
{
_tapRangeStart = selectedDate;
if (SelectionMode == CalendarSelectionMode.SingleRange)
{
foreach (DateTime item in SelectedDates)
{
RemovedItems.Add(item);
}
SelectedDates.ClearInternal();
}
if (!SelectedDates.Contains(selectedDate))
{
SelectedDates.Add(selectedDate);
}
return true;
}
if (SelectionMode == CalendarSelectionMode.SingleRange)
{
foreach (DateTime item in SelectedDates)
{
RemovedItems.Add(item);
}
SelectedDates.ClearInternal();
}
SelectedDates.AddRange(startDate, endDate);
_isTapRangeSelectionActive = false;
_tapRangeStart = null;
return true;
}
}
private void ProcessSelection(bool shift, DateTime? lastSelectedDate, int? index)
{
if (SelectionMode == CalendarSelectionMode.None && lastSelectedDate != null)
@ -1457,6 +1580,17 @@ namespace Avalonia.Controls
OnDayClick(lastSelectedDate.Value);
return;
}
// Handle tap range selection.
if (lastSelectedDate != null && index == null && !shift)
{
if (ProcessTapRangeSelection(lastSelectedDate.Value))
{
OnDayClick(lastSelectedDate.Value);
return;
}
}
if (lastSelectedDate != null && IsValidKeyboardSelection(this, lastSelectedDate.Value))
{
if (SelectionMode == CalendarSelectionMode.SingleRange || SelectionMode == CalendarSelectionMode.MultipleRange)
@ -2069,6 +2203,7 @@ namespace Avalonia.Controls
IsTodayHighlightedProperty.Changed.AddClassHandler<Calendar>((x, e) => x.OnIsTodayHighlightedChanged(e));
DisplayModeProperty.Changed.AddClassHandler<Calendar>((x, e) => x.OnDisplayModePropertyChanged(e));
SelectionModeProperty.Changed.AddClassHandler<Calendar>((x, e) => x.OnSelectionModeChanged(e));
AllowTapRangeSelectionProperty.Changed.AddClassHandler<Calendar>((x, e) => x.OnAllowTapRangeSelectionChanged(e));
SelectedDateProperty.Changed.AddClassHandler<Calendar>((x, e) => x.OnSelectedDateChanged(e));
DisplayDateProperty.Changed.AddClassHandler<Calendar>((x, e) => x.OnDisplayDateChanged(e));
DisplayDateStartProperty.Changed.AddClassHandler<Calendar>((x, e) => x.OnDisplayDateStartChanged(e));

9
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -1030,6 +1030,15 @@ namespace Avalonia.Controls.Primitives
Owner.OnDayClick(selectedDate);
return;
}
if (Owner.AllowTapRangeSelection &&
(Owner.SelectionMode == CalendarSelectionMode.SingleRange || Owner.SelectionMode == CalendarSelectionMode.MultipleRange))
{
if (Owner.ProcessTapRangeSelection(selectedDate))
{
Owner.OnDayClick(selectedDate);
return;
}
}
if (Owner.HoverStart.HasValue)
{
switch (Owner.SelectionMode)

55
src/Avalonia.Controls/Chrome/CaptionButtons.cs

@ -21,6 +21,8 @@ namespace Avalonia.Controls.Chrome
internal const string PART_FullScreenButton = "PART_FullScreenButton";
private Button? _restoreButton;
private Button? _minimizeButton;
private Button? _fullScreenButton;
private IDisposable? _disposables;
/// <summary>
@ -36,11 +38,16 @@ namespace Avalonia.Controls.Chrome
_disposables = new CompositeDisposable
{
HostWindow.GetObservable(Window.CanResizeProperty)
.Subscribe(x =>
HostWindow.GetObservable(Window.CanMaximizeProperty)
.Subscribe(_ =>
{
UpdateRestoreButtonState();
UpdateFullScreenButtonState();
}),
HostWindow.GetObservable(Window.CanMinimizeProperty)
.Subscribe(_ =>
{
if (_restoreButton is not null)
_restoreButton.IsEnabled = x;
UpdateMinimizeButtonState();
}),
HostWindow.GetObservable(Window.WindowStateProperty)
.Subscribe(x =>
@ -49,6 +56,9 @@ namespace Avalonia.Controls.Chrome
PseudoClasses.Set(":normal", x == WindowState.Normal);
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
UpdateRestoreButtonState();
UpdateMinimizeButtonState();
UpdateFullScreenButtonState();
}),
};
}
@ -116,8 +126,8 @@ namespace Avalonia.Controls.Chrome
OnRestore();
args.Handled = true;
};
restoreButton.IsEnabled = HostWindow?.CanResize ?? true;
_restoreButton = restoreButton;
UpdateRestoreButtonState();
}
if (e.NameScope.Find<Button>(PART_MinimizeButton) is { } minimizeButton)
@ -127,6 +137,8 @@ namespace Avalonia.Controls.Chrome
OnMinimize();
args.Handled = true;
};
_minimizeButton = minimizeButton;
UpdateMinimizeButtonState();
}
if (e.NameScope.Find<Button>(PART_FullScreenButton) is { } fullScreenButton)
@ -136,7 +148,40 @@ namespace Avalonia.Controls.Chrome
OnToggleFullScreen();
args.Handled = true;
};
_fullScreenButton = fullScreenButton;
UpdateFullScreenButtonState();
}
}
private void UpdateRestoreButtonState()
{
if (_restoreButton is null)
return;
_restoreButton.IsEnabled = HostWindow?.WindowState switch
{
WindowState.Maximized or WindowState.FullScreen => HostWindow.CanResize,
WindowState.Normal => HostWindow.CanMaximize,
_ => true
};
}
private void UpdateMinimizeButtonState()
{
if (_minimizeButton is null)
return;
_minimizeButton.IsEnabled = HostWindow?.CanMinimize ?? true;
}
private void UpdateFullScreenButtonState()
{
if (_fullScreenButton is null)
return;
_fullScreenButton.IsEnabled = HostWindow?.WindowState == WindowState.FullScreen ?
HostWindow.CanResize :
HostWindow?.CanMaximize ?? true;
}
}
}

166
src/Avalonia.Controls/ComboBox.cs

@ -5,9 +5,9 @@ using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Metadata;
@ -20,6 +20,7 @@ namespace Avalonia.Controls
/// A drop-down list control.
/// </summary>
[TemplatePart("PART_Popup", typeof(Popup), IsRequired = true)]
[TemplatePart("PART_EditableTextBox", typeof(TextBox), IsRequired = false)]
[PseudoClasses(pcDropdownOpen, pcPressed)]
public class ComboBox : SelectingItemsControl
{
@ -38,6 +39,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> IsDropDownOpenProperty =
AvaloniaProperty.Register<ComboBox, bool>(nameof(IsDropDownOpen));
/// <summary>
/// Defines the <see cref="IsEditable"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsEditableProperty =
AvaloniaProperty.Register<ComboBox, bool>(nameof(IsEditable));
/// <summary>
/// Defines the <see cref="MaxDropDownHeight"/> property.
/// </summary>
@ -73,7 +80,13 @@ namespace Avalonia.Controls
/// </summary>
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>();
/// <summary>
/// Defines the <see cref="Text"/> property
/// </summary>
public static readonly StyledProperty<string?> TextProperty =
TextBlock.TextProperty.AddOwner<ComboBox>(new(string.Empty, BindingMode.TwoWay));
/// <summary>
/// Defines the <see cref="SelectionBoxItemTemplate"/> property.
/// </summary>
@ -95,6 +108,10 @@ namespace Avalonia.Controls
private object? _selectionBoxItem;
private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
private TextBox? _inputTextBox;
private BindingEvaluator<string?>? _textValueBindingEvaluator = null;
private bool _skipNextTextChanged = false;
/// <summary>
/// Initializes static members of the <see cref="ComboBox"/> class.
/// </summary>
@ -124,6 +141,15 @@ namespace Avalonia.Controls
set => SetValue(IsDropDownOpenProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the control is editable
/// </summary>
public bool IsEditable
{
get => GetValue(IsEditableProperty);
set => SetValue(IsEditableProperty, value);
}
/// <summary>
/// Gets or sets the maximum height for the dropdown list.
/// </summary>
@ -188,6 +214,16 @@ namespace Avalonia.Controls
set => SetValue(SelectionBoxItemTemplateProperty, value);
}
/// <summary>
/// Gets or sets the text used when <see cref="IsEditable"/> is true.
/// Does nothing if not <see cref="IsEditable"/>.
/// </summary>
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
@ -229,7 +265,7 @@ namespace Avalonia.Controls
SetCurrentValue(IsDropDownOpenProperty, false);
e.Handled = true;
}
else if (!IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space))
else if (!IsDropDownOpen && !IsEditable && (e.Key == Key.Enter || e.Key == Key.Space))
{
SetCurrentValue(IsDropDownOpenProperty, true);
e.Handled = true;
@ -315,6 +351,15 @@ namespace Avalonia.Controls
/// <inheritdoc/>
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
//if the user clicked in the input text we don't want to open the dropdown
if (_inputTextBox != null
&& !e.Handled
&& e.Source is StyledElement styledSource
&& styledSource.TemplatedParent == _inputTextBox)
{
return;
}
if (!e.Handled && e.Source is Visual source)
{
if (_popup?.IsInsidePopup(source) == true)
@ -348,6 +393,8 @@ namespace Avalonia.Controls
_popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
_inputTextBox = e.NameScope.Find<TextBox>("PART_EditableTextBox");
}
/// <inheritdoc/>
@ -357,6 +404,7 @@ namespace Avalonia.Controls
{
UpdateSelectionBoxItem(change.NewValue);
TryFocusSelectedItem();
UpdateInputTextFromSelection(change.NewValue);
}
else if (change.Property == IsDropDownOpenProperty)
{
@ -366,6 +414,30 @@ namespace Avalonia.Controls
{
CoerceValue(SelectionBoxItemTemplateProperty);
}
else if (change.Property == IsEditableProperty && change.GetNewValue<bool>())
{
UpdateInputTextFromSelection(SelectedItem);
}
else if (change.Property == TextProperty)
{
TextChanged(change.GetNewValue<string>());
}
else if (change.Property == ItemsSourceProperty)
{
//the base handler deselects the current item (and resets Text) so we want to run the base first, then try match by text
string? text = Text;
base.OnPropertyChanged(change);
SetCurrentValue(TextProperty, text);
return;
}
else if (change.Property == DisplayMemberBindingProperty)
{
HandleTextValueBindingValueChanged(null, change);
}
else if (change.Property == TextSearch.TextBindingProperty)
{
HandleTextValueBindingValueChanged(change, null);
}
base.OnPropertyChanged(change);
}
@ -374,6 +446,17 @@ namespace Avalonia.Controls
return new ComboBoxAutomationPeer(this);
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
if (IsEditable && _inputTextBox != null)
{
_inputTextBox.Focus();
_inputTextBox.SelectAll();
}
base.OnGotFocus(e);
}
internal void ItemFocused(ComboBoxItem dropDownItem)
{
if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
@ -386,6 +469,11 @@ namespace Avalonia.Controls
{
_subscriptionsOnOpen.Clear();
if(IsEditable && CanFocus(this))
{
Focus();
}
DropDownClosed?.Invoke(this, EventArgs.Empty);
}
@ -502,6 +590,14 @@ namespace Avalonia.Controls
}
}
private void UpdateInputTextFromSelection(object? item)
{
//if we are modifying the text box which has deselected a value we don't want to update the textbox value
if (_skipNextTextChanged)
return;
SetCurrentValue(TextProperty, GetItemTextValue(item));
}
private void SelectFocusedItem()
{
foreach (var dropdownItem in GetRealizedContainers())
@ -561,5 +657,69 @@ namespace Avalonia.Controls
SelectedItem = null;
SelectedIndex = -1;
}
private void HandleTextValueBindingValueChanged(AvaloniaPropertyChangedEventArgs? textSearchPropChange,
AvaloniaPropertyChangedEventArgs? displayMemberPropChange)
{
IBinding? textValueBinding;
//prioritise using the TextSearch.TextBindingProperty if possible
if (textSearchPropChange == null && TextSearch.GetTextBinding(this) is IBinding textSearchBinding)
textValueBinding = textSearchBinding;
else if (textSearchPropChange != null && textSearchPropChange.NewValue is IBinding eventTextSearchBinding)
textValueBinding = eventTextSearchBinding;
else if (displayMemberPropChange != null && displayMemberPropChange.NewValue is IBinding eventDisplayMemberBinding)
textValueBinding = eventDisplayMemberBinding;
else
textValueBinding = null;
if (_textValueBindingEvaluator == null)
_textValueBindingEvaluator = BindingEvaluator<string?>.TryCreate(textValueBinding);
else if (textValueBinding == null)
_textValueBindingEvaluator = null;
else
_textValueBindingEvaluator.UpdateBinding(textValueBinding);
//if the binding is set we want to set the initial value for the selected item so the text box has the correct value
if (_textValueBindingEvaluator != null)
_textValueBindingEvaluator.Value = GetItemTextValue(SelectedValue);
}
private void TextChanged(string? newValue)
{
if (!IsEditable || _skipNextTextChanged)
return;
int selectedIdx = -1;
object? selectedItem = null;
int i = -1;
foreach (object? item in Items)
{
i++;
string itemText = GetItemTextValue(item);
if (string.Equals(newValue, itemText, StringComparison.CurrentCultureIgnoreCase))
{
selectedIdx = i;
selectedItem = item;
break;
}
}
_skipNextTextChanged = true;
try
{
SelectedIndex = selectedIdx;
SelectedItem = selectedItem;
}
finally
{
_skipNextTextChanged = false;
}
}
private string GetItemTextValue(object? item)
=> TextSearch.GetEffectiveText(item, _textValueBindingEvaluator);
}
}

357
src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs

@ -1,6 +1,5 @@
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using System;
@ -13,23 +12,23 @@ namespace Avalonia.Controls
/// Defines the presenter used for selecting a date for a
/// <see cref="DatePicker"/>
/// </summary>
[TemplatePart("PART_AcceptButton", typeof(Button), IsRequired = true)]
[TemplatePart("PART_DayDownButton", typeof(RepeatButton))]
[TemplatePart("PART_DayHost", typeof(Panel), IsRequired = true)]
[TemplatePart("PART_DaySelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_DayUpButton", typeof(RepeatButton))]
[TemplatePart("PART_DismissButton", typeof(Button))]
[TemplatePart("PART_FirstSpacer", typeof(Rectangle))]
[TemplatePart("PART_MonthDownButton", typeof(RepeatButton))]
[TemplatePart("PART_MonthHost", typeof(Panel), IsRequired = true)]
[TemplatePart("PART_MonthSelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_MonthUpButton", typeof(RepeatButton))]
[TemplatePart("PART_PickerContainer", typeof(Grid), IsRequired = true)]
[TemplatePart("PART_SecondSpacer", typeof(Rectangle))]
[TemplatePart("PART_YearDownButton", typeof(RepeatButton))]
[TemplatePart("PART_YearHost", typeof(Panel), IsRequired = true)]
[TemplatePart("PART_YearSelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_YearUpButton", typeof(RepeatButton))]
[TemplatePart(TemplateItems.AcceptButtonName, typeof(Button), IsRequired = true)]
[TemplatePart(TemplateItems.DayDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.DayHostName, typeof(Panel), IsRequired = true)]
[TemplatePart(TemplateItems.DaySelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TemplateItems.DayUpButtonName, typeof(Button))]
[TemplatePart(TemplateItems.DismissButtonName, typeof(Button))]
[TemplatePart(TemplateItems.FirstSpacerName, typeof(Control))]
[TemplatePart(TemplateItems.MonthDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.MonthHostName, typeof(Panel), IsRequired = true)]
[TemplatePart(TemplateItems.MonthSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TemplateItems.MonthUpButtonName, typeof(Button))]
[TemplatePart(TemplateItems.PickerContainerName, typeof(Grid), IsRequired = true)]
[TemplatePart(TemplateItems.SecondSpacerName, typeof(Control))]
[TemplatePart(TemplateItems.YearDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.YearHostName, typeof(Panel), IsRequired = true)]
[TemplatePart(TemplateItems.YearSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TemplateItems.YearUpButtonName, typeof(Button))]
public class DatePickerPresenter : PickerPresenterBase
{
/// <summary>
@ -102,24 +101,61 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> YearVisibleProperty =
DatePicker.YearVisibleProperty.AddOwner<DatePickerPresenter>();
// Template Items
private Grid? _pickerContainer;
private Button? _acceptButton;
private Button? _dismissButton;
private Rectangle? _spacer1;
private Rectangle? _spacer2;
private Panel? _monthHost;
private Panel? _yearHost;
private Panel? _dayHost;
private DateTimePickerPanel? _monthSelector;
private DateTimePickerPanel? _yearSelector;
private DateTimePickerPanel? _daySelector;
private Button? _monthUpButton;
private Button? _dayUpButton;
private Button? _yearUpButton;
private Button? _monthDownButton;
private Button? _dayDownButton;
private Button? _yearDownButton;
private struct TemplateItems
{
public Grid _pickerContainer;
public const string PickerContainerName = "PART_PickerContainer";
public Button _acceptButton;
public const string AcceptButtonName = "PART_AcceptButton";
public Button? _dismissButton;
public const string DismissButtonName = "PART_DismissButton";
public Control? _firstSpacer;
public const string FirstSpacerName = "PART_FirstSpacer";
public Control? _secondSpacer;
public const string SecondSpacerName = "PART_SecondSpacer";
public Panel _monthHost;
public const string MonthHostName = "PART_MonthHost";
public Panel _yearHost;
public const string YearHostName = "PART_YearHost";
public Panel _dayHost;
public const string DayHostName = "PART_DayHost";
public DateTimePickerPanel _monthSelector;
public const string MonthSelectorName = "PART_MonthSelector";
public DateTimePickerPanel _yearSelector;
public const string YearSelectorName = "PART_YearSelector";
public DateTimePickerPanel _daySelector;
public const string DaySelectorName = "PART_DaySelector";
public Button? _monthUpButton;
public const string MonthUpButtonName = "PART_MonthUpButton";
public Button? _dayUpButton;
public const string DayUpButtonName = "PART_DayUpButton";
public Button? _yearUpButton;
public const string YearUpButtonName = "PART_YearUpButton";
public Button? _monthDownButton;
public const string MonthDownButtonName = "PART_MonthDownButton";
public Button? _dayDownButton;
public const string DayDownButtonName = "PART_DayDownButton";
public Button? _yearDownButton;
public const string YearDownButtonName = "PART_YearDownButton";
}
private TemplateItems? _templateItems;
private DateTimeOffset _syncDate;
@ -235,69 +271,54 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
// These are requirements, so throw if not found
_pickerContainer = e.NameScope.Get<Grid>("PART_PickerContainer");
_monthHost = e.NameScope.Get<Panel>("PART_MonthHost");
_dayHost = e.NameScope.Get<Panel>("PART_DayHost");
_yearHost = e.NameScope.Get<Panel>("PART_YearHost");
_monthSelector = e.NameScope.Get<DateTimePickerPanel>("PART_MonthSelector");
_monthSelector.SelectionChanged += OnMonthChanged;
_daySelector = e.NameScope.Get<DateTimePickerPanel>("PART_DaySelector");
_daySelector.SelectionChanged += OnDayChanged;
_yearSelector = e.NameScope.Get<DateTimePickerPanel>("PART_YearSelector");
_yearSelector.SelectionChanged += OnYearChanged;
_acceptButton = e.NameScope.Get<Button>("PART_AcceptButton");
_monthUpButton = e.NameScope.Find<RepeatButton>("PART_MonthUpButton");
if (_monthUpButton != null)
{
_monthUpButton.Click += OnSelectorButtonClick;
}
_monthDownButton = e.NameScope.Find<RepeatButton>("PART_MonthDownButton");
if (_monthDownButton != null)
_templateItems = new()
{
_monthDownButton.Click += OnSelectorButtonClick;
}
// These are requirements, so throw if not found
_pickerContainer = e.NameScope.Get<Grid>(TemplateItems.PickerContainerName),
_monthHost = e.NameScope.Get<Panel>(TemplateItems.MonthHostName),
_dayHost = e.NameScope.Get<Panel>(TemplateItems.DayHostName),
_yearHost = e.NameScope.Get<Panel>(TemplateItems.YearHostName),
_monthSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.MonthSelectorName),
_daySelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.DaySelectorName),
_yearSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.YearSelectorName),
_acceptButton = e.NameScope.Get<Button>(TemplateItems.AcceptButtonName),
_monthUpButton = SelectorButton(TemplateItems.MonthUpButtonName, DateTimePickerPanelType.Month, SpinDirection.Decrease),
_monthDownButton = SelectorButton(TemplateItems.MonthDownButtonName, DateTimePickerPanelType.Month, SpinDirection.Increase),
_dayUpButton = SelectorButton(TemplateItems.DayUpButtonName, DateTimePickerPanelType.Day, SpinDirection.Decrease),
_dayDownButton = SelectorButton(TemplateItems.DayDownButtonName, DateTimePickerPanelType.Day, SpinDirection.Increase),
_yearUpButton = SelectorButton(TemplateItems.YearUpButtonName, DateTimePickerPanelType.Year, SpinDirection.Decrease),
_yearDownButton = SelectorButton(TemplateItems.YearDownButtonName, DateTimePickerPanelType.Year, SpinDirection.Increase),
_dismissButton = e.NameScope.Find<Button>(TemplateItems.DismissButtonName),
_firstSpacer = e.NameScope.Find<Control>(TemplateItems.FirstSpacerName),
_secondSpacer = e.NameScope.Find<Control>(TemplateItems.SecondSpacerName),
};
_dayUpButton = e.NameScope.Find<RepeatButton>("PART_DayUpButton");
if (_dayUpButton != null)
{
_dayUpButton.Click += OnSelectorButtonClick;
}
_dayDownButton = e.NameScope.Find<RepeatButton>("PART_DayDownButton");
if (_dayDownButton != null)
{
_dayDownButton.Click += OnSelectorButtonClick;
}
_templateItems.Value._acceptButton.Click += OnAcceptButtonClicked;
_templateItems.Value._monthSelector.SelectionChanged += OnMonthChanged;
_templateItems.Value._daySelector.SelectionChanged += OnDayChanged;
_templateItems.Value._yearSelector.SelectionChanged += OnYearChanged;
_yearUpButton = e.NameScope.Find<RepeatButton>("PART_YearUpButton");
if (_yearUpButton != null)
if (_templateItems.Value._dismissButton is { } dismissButton)
{
_yearUpButton.Click += OnSelectorButtonClick;
}
_yearDownButton = e.NameScope.Find<RepeatButton>("PART_YearDownButton");
if (_yearDownButton != null)
{
_yearDownButton.Click += OnSelectorButtonClick;
dismissButton.Click += OnDismissButtonClicked;
}
_dismissButton = e.NameScope.Find<Button>("PART_DismissButton");
_spacer1 = e.NameScope.Find<Rectangle>("PART_FirstSpacer");
_spacer2 = e.NameScope.Find<Rectangle>("PART_SecondSpacer");
InitPicker();
if (_acceptButton != null)
{
_acceptButton.Click += OnAcceptButtonClicked;
}
if (_dismissButton != null)
Button? SelectorButton(string name, DateTimePickerPanelType type, SpinDirection direction)
{
_dismissButton.Click += OnDismissButtonClicked;
if (e.NameScope.Find<Button>(name) is { } button)
{
button.Click += (s, e) => OnSelectorButtonClick(type, direction);
return button;
}
return null;
}
InitPicker();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@ -350,63 +371,63 @@ namespace Avalonia.Controls
private void InitPicker()
{
// OnApplyTemplate must've been called before we can init here...
if (_pickerContainer == null)
if (_templateItems is not { } items)
return;
_suppressUpdateSelection = true;
_monthSelector!.MaximumValue = 12;
_monthSelector.MinimumValue = 1;
_monthSelector.ItemFormat = MonthFormat;
items._monthSelector.MaximumValue = 12;
items._monthSelector.MinimumValue = 1;
items._monthSelector.ItemFormat = MonthFormat;
_daySelector!.ItemFormat = DayFormat;
items._daySelector.ItemFormat = DayFormat;
_yearSelector!.MaximumValue = MaxYear.Year;
_yearSelector.MinimumValue = MinYear.Year;
_yearSelector.ItemFormat = YearFormat;
items._yearSelector.MaximumValue = MaxYear.Year;
items._yearSelector.MinimumValue = MinYear.Year;
items._yearSelector.ItemFormat = YearFormat;
SetGrid();
SetGrid(items);
// Date should've been set when we reach this point
var dt = Date;
if (DayVisible)
{
_daySelector.FormatDate = dt.Date;
items._daySelector.FormatDate = dt.Date;
var maxDays = _calendar.GetDaysInMonth(dt.Year, dt.Month);
_daySelector.MaximumValue = maxDays;
_daySelector.MinimumValue = 1;
_daySelector.SelectedValue = dt.Day;
items._daySelector.MaximumValue = maxDays;
items._daySelector.MinimumValue = 1;
items._daySelector.SelectedValue = dt.Day;
}
if (MonthVisible)
{
_monthSelector.SelectedValue = dt.Month;
_monthSelector.FormatDate = dt.Date;
items._monthSelector.SelectedValue = dt.Month;
items._monthSelector.FormatDate = dt.Date;
}
if (YearVisible)
{
_yearSelector.SelectedValue = dt.Year;
_yearSelector.FormatDate = dt.Date;
items._yearSelector.SelectedValue = dt.Year;
items._yearSelector.FormatDate = dt.Date;
}
_suppressUpdateSelection = false;
SetInitialFocus();
SetInitialFocus(items);
}
private void SetGrid()
private void SetGrid(TemplateItems items)
{
var fmt = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
var columns = new List<(Panel?, int)>
{
(_monthHost, MonthVisible ? fmt.IndexOf("m", StringComparison.OrdinalIgnoreCase) : -1),
(_yearHost, YearVisible ? fmt.IndexOf("y", StringComparison.OrdinalIgnoreCase) : -1),
(_dayHost, DayVisible ? fmt.IndexOf("d", StringComparison.OrdinalIgnoreCase) : -1),
(items._monthHost, MonthVisible ? fmt.IndexOf("m", StringComparison.OrdinalIgnoreCase) : -1),
(items._yearHost, YearVisible ? fmt.IndexOf("y", StringComparison.OrdinalIgnoreCase) : -1),
(items._dayHost, DayVisible ? fmt.IndexOf("d", StringComparison.OrdinalIgnoreCase) : -1),
};
columns.Sort((x, y) => x.Item2 - y.Item2);
_pickerContainer!.ColumnDefinitions.Clear();
items._pickerContainer.ColumnDefinitions.Clear();
var columnIndex = 0;
@ -421,48 +442,53 @@ namespace Avalonia.Controls
{
if (columnIndex > 0)
{
_pickerContainer.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
items._pickerContainer.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
}
_pickerContainer.ColumnDefinitions.Add(
new ColumnDefinition(column.Item1 == _monthHost ? 138 : 78, GridUnitType.Star));
items._pickerContainer.ColumnDefinitions.Add(
new ColumnDefinition(column.Item1 == items._monthHost ? 138 : 78, GridUnitType.Star));
if (column.Item1.Parent is null)
{
_pickerContainer.Children.Add(column.Item1);
items._pickerContainer.Children.Add(column.Item1);
}
Grid.SetColumn(column.Item1, (columnIndex++ * 2));
}
}
var isSpacer1Visible = columnIndex > 1;
var isSpacer2Visible = columnIndex > 2;
// ternary conditional operator is used to make sure grid cells will be validated
Grid.SetColumn(_spacer1!, isSpacer1Visible ? 1 : 0);
Grid.SetColumn(_spacer2!, isSpacer2Visible ? 3 : 0);
ConfigureSpacer(items._firstSpacer, columnIndex > 1);
ConfigureSpacer(items._secondSpacer, columnIndex > 2);
static void ConfigureSpacer(Control? spacer, bool visible)
{
if (spacer == null)
return;
// ternary conditional operator is used to make sure grid cells will be validated
Grid.SetColumn(spacer, visible ? 1 : 0);
spacer.IsVisible = visible;
_spacer1!.IsVisible = isSpacer1Visible;
_spacer2!.IsVisible = isSpacer2Visible;
}
}
private void SetInitialFocus()
private void SetInitialFocus(TemplateItems items)
{
int monthCol = MonthVisible ? Grid.GetColumn(_monthHost!) : int.MaxValue;
int dayCol = DayVisible ? Grid.GetColumn(_dayHost!) : int.MaxValue;
int yearCol = YearVisible ? Grid.GetColumn(_yearHost!) : int.MaxValue;
int monthCol = MonthVisible ? Grid.GetColumn(items._monthHost) : int.MaxValue;
int dayCol = DayVisible ? Grid.GetColumn(items._dayHost) : int.MaxValue;
int yearCol = YearVisible ? Grid.GetColumn(items._yearHost) : int.MaxValue;
if (monthCol < dayCol && monthCol < yearCol)
{
_monthSelector?.Focus(NavigationMethod.Pointer);
items._monthSelector.Focus(NavigationMethod.Pointer);
}
else if (dayCol < monthCol && dayCol < yearCol)
{
_monthSelector?.Focus(NavigationMethod.Pointer);
items._monthSelector.Focus(NavigationMethod.Pointer);
}
else if (yearCol < monthCol && yearCol < dayCol)
{
_yearSelector?.Focus(NavigationMethod.Pointer);
items._yearSelector.Focus(NavigationMethod.Pointer);
}
}
@ -477,29 +503,36 @@ namespace Avalonia.Controls
OnConfirmed();
}
private void OnSelectorButtonClick(object? sender, RoutedEventArgs e)
private void OnSelectorButtonClick(DateTimePickerPanelType type, SpinDirection direction)
{
if (sender == _monthUpButton)
_monthSelector!.ScrollUp();
else if (sender == _monthDownButton)
_monthSelector!.ScrollDown();
else if (sender == _yearUpButton)
_yearSelector!.ScrollUp();
else if (sender == _yearDownButton)
_yearSelector!.ScrollDown();
else if (sender == _dayUpButton)
_daySelector!.ScrollUp();
else if (sender == _dayDownButton)
_daySelector!.ScrollDown();
var target = type switch
{
DateTimePickerPanelType.Month => _templateItems?._monthSelector,
DateTimePickerPanelType.Day => _templateItems?._daySelector,
DateTimePickerPanelType.Year=> _templateItems?._yearSelector,
_ => throw new NotImplementedException(),
};
switch (direction)
{
case SpinDirection.Increase:
target?.ScrollDown();
break;
case SpinDirection.Decrease:
target?.ScrollUp();
break;
default:
throw new NotImplementedException();
}
}
private void OnYearChanged(object? sender, EventArgs e)
{
if (_suppressUpdateSelection)
if (_suppressUpdateSelection || _templateItems is not { } items)
return;
int maxDays = _calendar.GetDaysInMonth(_yearSelector!.SelectedValue, _syncDate.Month);
var newDate = new DateTimeOffset(_yearSelector.SelectedValue, _syncDate.Month,
int maxDays = _calendar.GetDaysInMonth(items._yearSelector.SelectedValue, _syncDate.Month);
var newDate = new DateTimeOffset(items._yearSelector.SelectedValue, _syncDate.Month,
_syncDate.Day > maxDays ? maxDays : _syncDate.Day, 0, 0, 0, _syncDate.Offset);
_syncDate = newDate;
@ -510,30 +543,30 @@ namespace Avalonia.Controls
_suppressUpdateSelection = true;
_daySelector!.FormatDate = newDate.Date;
items._daySelector.FormatDate = newDate.Date;
if (_daySelector.MaximumValue != maxDays)
_daySelector.MaximumValue = maxDays;
if (items._daySelector.MaximumValue != maxDays)
items._daySelector.MaximumValue = maxDays;
else
_daySelector.RefreshItems();
items._daySelector.RefreshItems();
_suppressUpdateSelection = false;
}
private void OnDayChanged(object? sender, EventArgs e)
{
if (_suppressUpdateSelection)
if (_suppressUpdateSelection || _templateItems is not { } items)
return;
_syncDate = new DateTimeOffset(_syncDate.Year, _syncDate.Month, _daySelector!.SelectedValue, 0, 0, 0, _syncDate.Offset);
_syncDate = new DateTimeOffset(_syncDate.Year, _syncDate.Month, items._daySelector.SelectedValue, 0, 0, 0, _syncDate.Offset);
}
private void OnMonthChanged(object? sender, EventArgs e)
{
if (_suppressUpdateSelection)
if (_suppressUpdateSelection || _templateItems is not { } items)
return;
int maxDays = _calendar.GetDaysInMonth(_syncDate.Year, _monthSelector!.SelectedValue);
var newDate = new DateTimeOffset(_syncDate.Year, _monthSelector.SelectedValue,
int maxDays = _calendar.GetDaysInMonth(_syncDate.Year, items._monthSelector.SelectedValue);
var newDate = new DateTimeOffset(_syncDate.Year, items._monthSelector.SelectedValue,
_syncDate.Day > maxDays ? maxDays : _syncDate.Day, 0, 0, 0, _syncDate.Offset);
if (!DayVisible)
@ -544,24 +577,24 @@ namespace Avalonia.Controls
_suppressUpdateSelection = true;
_daySelector!.FormatDate = newDate.Date;
items._daySelector.FormatDate = newDate.Date;
_syncDate = newDate;
if (_daySelector.MaximumValue != maxDays)
_daySelector.MaximumValue = maxDays;
if (items._daySelector.MaximumValue != maxDays)
items._daySelector.MaximumValue = maxDays;
else
_daySelector.RefreshItems();
items._daySelector.RefreshItems();
_suppressUpdateSelection = false;
}
internal double GetOffsetForPopup()
{
if (_monthSelector is null)
if (_templateItems is not { } items)
return 0;
var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41;
return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_monthSelector.ItemHeight / 2);
var acceptDismissButtonHeight = items._acceptButton.Bounds.Height;
return -(MaxHeight - acceptDismissButtonHeight) / 2 - (items._monthSelector.ItemHeight / 2);
}
}
}

382
src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs

@ -1,9 +1,8 @@
using Avalonia.Controls.Metadata;
using System;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using System;
namespace Avalonia.Controls
{
@ -11,25 +10,25 @@ namespace Avalonia.Controls
/// Defines the presenter used for selecting a time. Intended for use with
/// <see cref="TimePicker"/> but can be used independently
/// </summary>
[TemplatePart("PART_AcceptButton", typeof(Button), IsRequired = true)]
[TemplatePart("PART_DismissButton", typeof(Button))]
[TemplatePart("PART_HourDownButton", typeof(RepeatButton))]
[TemplatePart("PART_HourSelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_HourUpButton", typeof(RepeatButton))]
[TemplatePart("PART_MinuteDownButton", typeof(RepeatButton))]
[TemplatePart("PART_MinuteSelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_MinuteUpButton", typeof(RepeatButton))]
[TemplatePart("PART_SecondDownButton", typeof(RepeatButton))]
[TemplatePart("PART_SecondHost", typeof(Panel), IsRequired = true)]
[TemplatePart("PART_SecondSelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_SecondUpButton", typeof(RepeatButton))]
[TemplatePart("PART_PeriodDownButton", typeof(RepeatButton))]
[TemplatePart("PART_PeriodHost", typeof(Panel), IsRequired = true)]
[TemplatePart("PART_PeriodSelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_PeriodUpButton", typeof(RepeatButton))]
[TemplatePart("PART_PickerContainer", typeof(Grid), IsRequired = true)]
[TemplatePart("PART_SecondSpacer", typeof(Rectangle), IsRequired = true)]
[TemplatePart("PART_ThirdSpacer", typeof(Rectangle), IsRequired = true)]
[TemplatePart(TemplateItems.AcceptButtonName, typeof(Button), IsRequired = true)]
[TemplatePart(TemplateItems.DismissButtonName, typeof(Button))]
[TemplatePart(TemplateItems.HourDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.HourSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TemplateItems.HourUpButtonName, typeof(Button))]
[TemplatePart(TemplateItems.MinuteDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.MinuteSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TemplateItems.MinuteUpButtonName, typeof(Button))]
[TemplatePart(TemplateItems.SecondDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.SecondHostName, typeof(Panel))]
[TemplatePart(TemplateItems.SecondSelectorName, typeof(DateTimePickerPanel))]
[TemplatePart(TemplateItems.SecondUpButtonName, typeof(Button))]
[TemplatePart(TemplateItems.PeriodDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.PeriodHostName, typeof(Panel), IsRequired = true)]
[TemplatePart(TemplateItems.PeriodSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TemplateItems.PeriodUpButtonName, typeof(Button))]
[TemplatePart(TemplateItems.PickerContainerName, typeof(Grid), IsRequired = true)]
[TemplatePart(TemplateItems.SecondSpacerName, typeof(Control), IsRequired = true)]
[TemplatePart(TemplateItems.ThirdSpacerName, typeof(Control))]
public class TimePickerPresenter : PickerPresenterBase
{
/// <summary>
@ -37,7 +36,7 @@ namespace Avalonia.Controls
/// </summary>
public static readonly StyledProperty<int> MinuteIncrementProperty =
TimePicker.MinuteIncrementProperty.AddOwner<TimePickerPresenter>();
/// <summary>
/// Defines the <see cref="SecondIncrement"/> property
/// </summary>
@ -49,7 +48,7 @@ namespace Avalonia.Controls
/// </summary>
public static readonly StyledProperty<string> ClockIdentifierProperty =
TimePicker.ClockIdentifierProperty.AddOwner<TimePickerPresenter>();
/// <summary>
/// Defines the <see cref="UseSeconds"/> property
/// </summary>
@ -72,26 +71,54 @@ namespace Avalonia.Controls
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue<TimePickerPresenter>(KeyboardNavigationMode.Cycle);
}
// TemplateItems
private Grid? _pickerContainer;
private Button? _acceptButton;
private Button? _dismissButton;
private Rectangle? _spacer2;
private Rectangle? _spacer3;
private Panel? _secondHost;
private Panel? _periodHost;
private DateTimePickerPanel? _hourSelector;
private DateTimePickerPanel? _minuteSelector;
private DateTimePickerPanel? _secondSelector;
private DateTimePickerPanel? _periodSelector;
private Button? _hourUpButton;
private Button? _minuteUpButton;
private Button? _secondUpButton;
private Button? _periodUpButton;
private Button? _hourDownButton;
private Button? _minuteDownButton;
private Button? _secondDownButton;
private Button? _periodDownButton;
private struct TemplateItems
{
public Grid _pickerContainer;
public const string PickerContainerName = "PART_PickerContainer";
public Button _acceptButton;
public const string AcceptButtonName = "PART_AcceptButton";
public Button? _dismissButton;
public const string DismissButtonName = "PART_DismissButton";
public Control _secondSpacer; // the 2nd spacer, not seconds of time
public const string SecondSpacerName = "PART_SecondSpacer";
public Control? _thirdSpacer;
public const string ThirdSpacerName = "PART_ThirdSpacer";
public Panel? _secondHost;
public const string SecondHostName = "PART_SecondHost";
public Panel _periodHost;
public const string PeriodHostName = "PART_PeriodHost";
public DateTimePickerPanel _hourSelector;
public const string HourSelectorName = "PART_HourSelector";
public DateTimePickerPanel _minuteSelector;
public const string MinuteSelectorName = "PART_MinuteSelector";
public DateTimePickerPanel? _secondSelector;
public const string SecondSelectorName = "PART_SecondSelector";
public DateTimePickerPanel _periodSelector;
public const string PeriodSelectorName = "PART_PeriodSelector";
public Button? _hourUpButton;
public const string HourUpButtonName = "PART_HourUpButton";
public Button? _minuteUpButton;
public const string MinuteUpButtonName = "PART_MinuteUpButton";
public Button? _secondUpButton;
public const string SecondUpButtonName = "PART_SecondUpButton";
public Button? _periodUpButton;
public const string PeriodUpButtonName = "PART_PeriodUpButton";
public Button? _hourDownButton;
public const string HourDownButtonName = "PART_HourDownButton";
public Button? _minuteDownButton;
public const string MinuteDownButtonName = "PART_MinuteDownButton";
public Button? _secondDownButton;
public const string SecondDownButtonName = "PART_SecondDownButton";
public Button? _periodDownButton;
public const string PeriodDownButtonName = "PART_PeriodDownButton";
}
private TemplateItems? _templateItems;
/// <summary>
/// Gets or sets the minute increment in the selector
@ -101,7 +128,7 @@ namespace Avalonia.Controls
get => GetValue(MinuteIncrementProperty);
set => SetValue(MinuteIncrementProperty, value);
}
/// <summary>
/// Gets or sets the second increment in the selector
/// </summary>
@ -119,7 +146,7 @@ namespace Avalonia.Controls
get => GetValue(ClockIdentifierProperty);
set => SetValue(ClockIdentifierProperty, value);
}
/// <summary>
/// Gets or sets the current clock identifier, either 12HourClock or 24HourClock
/// </summary>
@ -142,54 +169,54 @@ namespace Avalonia.Controls
{
base.OnApplyTemplate(e);
_pickerContainer = e.NameScope.Get<Grid>("PART_PickerContainer");
_periodHost = e.NameScope.Get<Panel>("PART_PeriodHost");
_secondHost = e.NameScope.Find<Panel>("PART_SecondHost");
_hourSelector = e.NameScope.Get<DateTimePickerPanel>("PART_HourSelector");
_minuteSelector = e.NameScope.Get<DateTimePickerPanel>("PART_MinuteSelector");
_secondSelector = e.NameScope.Find<DateTimePickerPanel>("PART_SecondSelector");
_periodSelector = e.NameScope.Get<DateTimePickerPanel>("PART_PeriodSelector");
_spacer2 = e.NameScope.Get<Rectangle>("PART_SecondSpacer");
_spacer3 = e.NameScope.Find<Rectangle>("PART_ThirdSpacer");
_acceptButton = e.NameScope.Get<Button>("PART_AcceptButton");
_acceptButton.Click += OnAcceptButtonClicked;
_hourUpButton = e.NameScope.Find<RepeatButton>("PART_HourUpButton");
if (_hourUpButton != null)
_hourUpButton.Click += OnSelectorButtonClick;
_hourDownButton = e.NameScope.Find<RepeatButton>("PART_HourDownButton");
if (_hourDownButton != null)
_hourDownButton.Click += OnSelectorButtonClick;
_minuteUpButton = e.NameScope.Find<RepeatButton>("PART_MinuteUpButton");
if (_minuteUpButton != null)
_minuteUpButton.Click += OnSelectorButtonClick;
_minuteDownButton = e.NameScope.Find<RepeatButton>("PART_MinuteDownButton");
if (_minuteDownButton != null)
_minuteDownButton.Click += OnSelectorButtonClick;
_secondUpButton = e.NameScope.Find<RepeatButton>("PART_SecondUpButton");
if (_secondUpButton != null)
_secondUpButton.Click += OnSelectorButtonClick;
_secondDownButton = e.NameScope.Find<RepeatButton>("PART_SecondDownButton");
if (_secondDownButton != null)
_secondDownButton.Click += OnSelectorButtonClick;
_periodUpButton = e.NameScope.Find<RepeatButton>("PART_PeriodUpButton");
if (_periodUpButton != null)
_periodUpButton.Click += OnSelectorButtonClick;
_periodDownButton = e.NameScope.Find<RepeatButton>("PART_PeriodDownButton");
if (_periodDownButton != null)
_periodDownButton.Click += OnSelectorButtonClick;
_dismissButton = e.NameScope.Find<Button>("PART_DismissButton");
if (_dismissButton != null)
_dismissButton.Click += OnDismissButtonClicked;
_templateItems = new()
{
_pickerContainer = e.NameScope.Get<Grid>(TemplateItems.PickerContainerName),
_periodHost = e.NameScope.Get<Panel>(TemplateItems.PeriodHostName),
_secondHost = e.NameScope.Find<Panel>(TemplateItems.SecondHostName),
_hourSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.HourSelectorName),
_minuteSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.MinuteSelectorName),
_secondSelector = e.NameScope.Find<DateTimePickerPanel>(TemplateItems.SecondSelectorName),
_periodSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.PeriodSelectorName),
_secondSpacer = e.NameScope.Get<Control>(TemplateItems.SecondSpacerName),
_thirdSpacer = e.NameScope.Find<Control>(TemplateItems.ThirdSpacerName),
_acceptButton = e.NameScope.Get<Button>(TemplateItems.AcceptButtonName),
_hourUpButton = SelectorButton(TemplateItems.HourUpButtonName, DateTimePickerPanelType.Hour, SpinDirection.Decrease),
_hourDownButton = SelectorButton(TemplateItems.HourDownButtonName, DateTimePickerPanelType.Hour, SpinDirection.Increase),
_minuteUpButton = SelectorButton(TemplateItems.MinuteUpButtonName, DateTimePickerPanelType.Minute, SpinDirection.Decrease),
_minuteDownButton = SelectorButton(TemplateItems.MinuteDownButtonName, DateTimePickerPanelType.Minute, SpinDirection.Increase),
_secondUpButton = SelectorButton(TemplateItems.SecondUpButtonName, DateTimePickerPanelType.Second, SpinDirection.Decrease),
_secondDownButton = SelectorButton(TemplateItems.SecondDownButtonName, DateTimePickerPanelType.Second, SpinDirection.Increase),
_periodUpButton = SelectorButton(TemplateItems.PeriodUpButtonName, DateTimePickerPanelType.TimePeriod, SpinDirection.Decrease),
_periodDownButton = SelectorButton(TemplateItems.PeriodDownButtonName, DateTimePickerPanelType.TimePeriod, SpinDirection.Increase),
_dismissButton = e.NameScope.Find<Button>(TemplateItems.DismissButtonName),
};
_templateItems.Value._acceptButton.Click += OnAcceptButtonClicked;
if (_templateItems.Value._dismissButton is { } dismissButton)
{
dismissButton.Click += OnDismissButtonClicked;
}
InitPicker();
Button? SelectorButton(string name, DateTimePickerPanelType type, SpinDirection direction)
{
if (e.NameScope.Find<Button>(name) is { } button)
{
button.Click += (s, e) => OnSelectorButtonClick(type, direction);
return button;
}
return null;
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@ -232,100 +259,105 @@ namespace Avalonia.Controls
protected override void OnConfirmed()
{
var hr = _hourSelector!.SelectedValue;
var min = _minuteSelector!.SelectedValue;
var sec = _secondSelector?.SelectedValue ?? 0;
var per = _periodSelector!.SelectedValue;
if (ClockIdentifier == "12HourClock")
if (_templateItems is { } items)
{
hr = per == 1 ? (hr == 12) ? 12 : hr + 12 : per == 0 && hr == 12 ? 0 : hr;
}
var hr = items._hourSelector.SelectedValue;
var min = items._minuteSelector.SelectedValue;
var sec = items._secondSelector?.SelectedValue ?? 0;
var per = items._periodSelector.SelectedValue;
SetCurrentValue(TimeProperty, new TimeSpan(hr, min, UseSeconds ? sec : 0));
if (ClockIdentifier == "12HourClock")
{
hr = per == 1 ? (hr == 12) ? 12 : hr + 12 : per == 0 && hr == 12 ? 0 : hr;
}
SetCurrentValue(TimeProperty, new TimeSpan(hr, min, UseSeconds ? sec : 0));
}
base.OnConfirmed();
}
private void InitPicker()
{
if (_pickerContainer == null)
if (_templateItems is not { } items)
return;
bool clock12 = ClockIdentifier == "12HourClock";
_hourSelector!.MaximumValue = clock12 ? 12 : 23;
_hourSelector.MinimumValue = clock12 ? 1 : 0;
_hourSelector.ItemFormat = "%h";
items._hourSelector.MaximumValue = clock12 ? 12 : 23;
items._hourSelector.MinimumValue = clock12 ? 1 : 0;
items._hourSelector.ItemFormat = "%h";
var hr = Time.Hours;
_hourSelector.SelectedValue = !clock12 ? hr :
items._hourSelector.SelectedValue = !clock12 ? hr :
hr > 12 ? hr - 12 : hr == 0 ? 12 : hr;
_minuteSelector!.MaximumValue = 59;
_minuteSelector.MinimumValue = 0;
_minuteSelector.Increment = MinuteIncrement;
_minuteSelector.ItemFormat = "mm";
_minuteSelector.SelectedValue = Time.Minutes;
items._minuteSelector.MaximumValue = 59;
items._minuteSelector.MinimumValue = 0;
items._minuteSelector.Increment = MinuteIncrement;
items._minuteSelector.ItemFormat = "mm";
items._minuteSelector.SelectedValue = Time.Minutes;
if (_secondSelector is not null)
if (items._secondSelector is { } secondSelector)
{
_secondSelector.MaximumValue = 59;
_secondSelector.MinimumValue = 0;
_secondSelector.Increment = SecondIncrement;
_secondSelector.ItemFormat = "ss";
_secondSelector.SelectedValue = Time.Seconds;
secondSelector.MaximumValue = 59;
secondSelector.MinimumValue = 0;
secondSelector.Increment = SecondIncrement;
secondSelector.ItemFormat = "ss";
secondSelector.SelectedValue = Time.Seconds;
}
_periodSelector!.MaximumValue = 1;
_periodSelector.MinimumValue = 0;
_periodSelector.SelectedValue = hr >= 12 ? 1 : 0;
items._periodSelector.MaximumValue = 1;
items._periodSelector.MinimumValue = 0;
items._periodSelector.SelectedValue = hr >= 12 ? 1 : 0;
SetGrid();
_hourSelector?.Focus(NavigationMethod.Pointer);
SetGrid(items);
items._hourSelector.Focus(NavigationMethod.Pointer);
}
private void SetGrid()
private void SetGrid(TemplateItems items)
{
var use24HourClock = ClockIdentifier == "24HourClock";
var canUseSeconds = _secondHost is not null && _spacer3 is not null;
var columnsD = new ColumnDefinitions();
columnsD.Add(new ColumnDefinition(GridLength.Star));
columnsD.Add(new ColumnDefinition(GridLength.Auto));
columnsD.Add(new ColumnDefinition(GridLength.Star));
if (canUseSeconds && UseSeconds)
{
columnsD.Add(new ColumnDefinition(GridLength.Auto));
columnsD.Add(new ColumnDefinition(GridLength.Star));
}
if (!use24HourClock)
{
columnsD.Add(new ColumnDefinition(GridLength.Auto));
columnsD.Add(new ColumnDefinition(GridLength.Star));
}
_pickerContainer!.ColumnDefinitions = columnsD;
var columnsD = new ColumnDefinitions
{
new(GridLength.Star),
new(GridLength.Auto),
new(GridLength.Star)
};
if (canUseSeconds)
if (items._secondHost is not null && items._thirdSpacer is not null)
{
_spacer2!.IsVisible = UseSeconds;
_secondHost!.IsVisible = UseSeconds;
_spacer3!.IsVisible = !use24HourClock;
_periodHost!.IsVisible = !use24HourClock;
if (UseSeconds)
{
columnsD.Add(new ColumnDefinition(GridLength.Auto));
columnsD.Add(new ColumnDefinition(GridLength.Star));
}
items._secondSpacer.IsVisible = UseSeconds;
items._secondHost.IsVisible = UseSeconds;
items._thirdSpacer.IsVisible = !use24HourClock;
items._periodHost.IsVisible = !use24HourClock;
var amPmColumn = (UseSeconds) ? 6 : 4;
Grid.SetColumn(_spacer2, UseSeconds ? 3 : 0);
Grid.SetColumn(_secondHost, UseSeconds ? 4 : 0);
Grid.SetColumn(_spacer3, use24HourClock ? 0 : amPmColumn-1);
Grid.SetColumn(_periodHost, use24HourClock ? 0 : amPmColumn);
Grid.SetColumn(items._secondSpacer, UseSeconds ? 3 : 0);
Grid.SetColumn(items._secondHost, UseSeconds ? 4 : 0);
Grid.SetColumn(items._thirdSpacer, use24HourClock ? 0 : amPmColumn - 1);
Grid.SetColumn(items._periodHost, use24HourClock ? 0 : amPmColumn);
}
else
{
_spacer2!.IsVisible = !use24HourClock;
_periodHost!.IsVisible = !use24HourClock;
Grid.SetColumn(_spacer2, use24HourClock ? 0 : 3);
Grid.SetColumn(_periodHost, use24HourClock ? 0 : 4);
items._secondSpacer.IsVisible = !use24HourClock;
items._periodHost.IsVisible = !use24HourClock;
Grid.SetColumn(items._secondSpacer, use24HourClock ? 0 : 3);
Grid.SetColumn(items._periodHost, use24HourClock ? 0 : 4);
}
if (!use24HourClock)
{
columnsD.Add(new ColumnDefinition(GridLength.Auto));
columnsD.Add(new ColumnDefinition(GridLength.Star));
}
items._pickerContainer.ColumnDefinitions = columnsD;
}
private void OnDismissButtonClicked(object? sender, RoutedEventArgs e)
@ -338,33 +370,37 @@ namespace Avalonia.Controls
OnConfirmed();
}
private void OnSelectorButtonClick(object? sender, RoutedEventArgs e)
private void OnSelectorButtonClick(DateTimePickerPanelType type, SpinDirection direction)
{
if (sender == _hourUpButton)
_hourSelector!.ScrollUp();
else if (sender == _hourDownButton)
_hourSelector!.ScrollDown();
else if (sender == _minuteUpButton)
_minuteSelector!.ScrollUp();
else if (sender == _minuteDownButton)
_minuteSelector!.ScrollDown();
else if (sender == _secondUpButton)
_secondSelector!.ScrollUp();
else if (sender == _secondDownButton)
_secondSelector!.ScrollDown();
else if (sender == _periodUpButton)
_periodSelector!.ScrollUp();
else if (sender == _periodDownButton)
_periodSelector!.ScrollDown();
var target = type switch
{
DateTimePickerPanelType.Hour => _templateItems?._hourSelector,
DateTimePickerPanelType.Minute => _templateItems?._minuteSelector,
DateTimePickerPanelType.Second => _templateItems?._secondSelector,
DateTimePickerPanelType.TimePeriod => _templateItems?._periodSelector,
_ => throw new NotImplementedException(),
};
switch (direction)
{
case SpinDirection.Increase:
target?.ScrollDown();
break;
case SpinDirection.Decrease:
target?.ScrollUp();
break;
default:
throw new NotImplementedException();
}
}
internal double GetOffsetForPopup()
{
if (_hourSelector is null)
if (_templateItems is not { } items)
return 0;
var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41;
return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_hourSelector.ItemHeight / 2);
var acceptDismissButtonHeight = items._acceptButton.Bounds.Height;
return -(MaxHeight - acceptDismissButtonHeight) / 2 - (items._hourSelector.ItemHeight / 2);
}
}
}

86
src/Avalonia.Controls/GridSplitter.cs

@ -7,6 +7,7 @@ using System;
using System.Diagnostics;
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
@ -211,7 +212,9 @@ namespace Avalonia.Controls
private void InitializeData(bool showsPreview)
{
// If not in a grid or can't resize, do nothing.
if (Parent is Grid grid)
var grid = GetParentGrid();
if (grid != null)
{
GridResizeDirection resizeDirection = GetEffectiveResizeDirection();
@ -244,13 +247,15 @@ namespace Avalonia.Controls
/// </summary>
private bool SetupDefinitionsToResize()
{
int gridSpan = GetValue(_resizeData!.ResizeDirection == GridResizeDirection.Columns ?
// Get properties values from ContentPresenter if Grid it's used in ItemsControl as ItemsPanel otherwise directly from GridSplitter.
var sourceControl = GetPropertiesValueSource();
int gridSpan = sourceControl.GetValue(_resizeData!.ResizeDirection == GridResizeDirection.Columns ?
Grid.ColumnSpanProperty :
Grid.RowSpanProperty);
if (gridSpan == 1)
{
var splitterIndex = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ?
var splitterIndex = sourceControl.GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ?
Grid.ColumnProperty :
Grid.RowProperty);
@ -351,6 +356,81 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Retrieves the <see cref="Grid"/> that ultimately hosts this
/// <see cref="GridSplitter"/> in the visual/logical tree.
/// </summary>
/// <remarks>
/// A splitter can be placed directly inside a <see cref="Grid"/> or
/// indirectly inside an <see cref="ItemsControl"/> that uses a
/// <see cref="Grid"/> as its <see cref="ItemsControl.ItemsPanel"/>.
/// In the latter case the first logical parent is usually an
/// <see cref="ContentPresenter"/> (or the items control itself),
/// so the method walks these intermediate containers to locate the
/// underlying grid.
/// </remarks>
/// <returns>
/// The containing <see cref="Grid"/> if one is found; otherwise
/// <c>null</c>.
/// </returns>
protected virtual Grid? GetParentGrid()
{
// When GridSplitter is used inside an ItemsControl with Grid as
// its ItemsPanel, its immediate parent is usually a ItemsControl or ContentPresenter.
switch (Parent)
{
case Grid grid:
{
return grid;
}
case ItemsControl itemsControl:
{
if (itemsControl.ItemsPanelRoot is Grid grid)
{
return grid;
}
break;
}
case ContentPresenter { Parent: ItemsControl presenterItemsControl }:
{
if (presenterItemsControl.ItemsPanelRoot is Grid grid)
{
return grid;
}
break;
}
}
return null;
}
/// <summary>
/// Returns the element that carries the grid-attached properties
/// (<see cref="Grid.RowProperty"/>, <see cref="Grid.ColumnProperty"/>, etc.) relevant
/// to this <see cref="GridSplitter"/>.
/// </summary>
/// <remarks>
/// When the splitter is generated as part of an <see cref="ItemsControl"/>
/// template, the attached properties are set on the surrounding
/// <see cref="ContentPresenter"/> rather than on the splitter itself.
/// This helper selects that presenter when appropriate so subsequent
/// property look-ups read the correct values; otherwise it simply
/// returns <c>this</c>.
/// </remarks>
/// <returns>
/// The <see cref="StyledElement"/> from which grid-attached properties
/// should be read—either the parent <see cref="ContentPresenter"/> or
/// the splitter instance.
/// </returns>
protected virtual StyledElement GetPropertiesValueSource()
{
return Parent is ContentPresenter
? Parent
: this;
}
protected override void OnPointerEntered(PointerEventArgs e)
{
base.OnPointerEntered(e);

11
src/Avalonia.Controls/Platform/IWindowImpl.cs

@ -64,6 +64,16 @@ namespace Avalonia.Platform
/// </summary>
void CanResize(bool value);
/// <summary>
/// Enables or disables minimizing the window.
/// </summary>
void SetCanMinimize(bool value);
/// <summary>
/// Enables or disables maximizing the window.
/// </summary>
void SetCanMaximize(bool value);
/// <summary>
/// Gets or sets a method called before the underlying implementation is destroyed.
/// Return true to prevent the underlying implementation from closing.
@ -124,7 +134,6 @@ namespace Avalonia.Platform
/// <summary>
/// Minimum width of the window.
/// </summary>
///
void SetMinMaxSize(Size minSize, Size maxSize);
/// <summary>

32
src/Avalonia.Controls/SplitView/SplitView.cs

@ -314,6 +314,7 @@ namespace Avalonia.Controls
{
base.OnDetachedFromVisualTree(e);
_pointerDisposable?.Dispose();
_pointerDisposable = null;
}
/// <inheritdoc/>
@ -423,7 +424,7 @@ namespace Avalonia.Controls
protected virtual void OnPaneOpened(RoutedEventArgs args)
{
EnableLightDismiss();
InvalidateLightDismissSubscription();
RaiseEvent(args);
}
@ -528,6 +529,8 @@ namespace Avalonia.Controls
};
TemplateSettings.ClosedPaneWidth = closedPaneWidth;
TemplateSettings.PaneColumnGridLength = paneColumnGridLength;
InvalidateLightDismissSubscription();
}
private void UpdateVisualStateForPanePlacementProperty(SplitViewPanePlacement newValue)
@ -541,7 +544,7 @@ namespace Avalonia.Controls
PseudoClasses.Add(_lastPlacementPseudoclass);
}
private void EnableLightDismiss()
private void InvalidateLightDismissSubscription()
{
if (_pane == null)
return;
@ -549,19 +552,26 @@ namespace Avalonia.Controls
// If this returns false, we're not in Overlay or CompactOverlay DisplayMode
// and don't need the light dismiss behavior
if (!IsInOverlayMode())
{
_pointerDisposable?.Dispose();
_pointerDisposable = null;
return;
}
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel != null)
if (_pointerDisposable == null)
{
_pointerDisposable = Disposable.Create(() =>
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel != null)
{
topLevel.PointerReleased -= PointerReleasedOutside;
topLevel.BackRequested -= TopLevelBackRequested;
});
topLevel.PointerReleased += PointerReleasedOutside;
topLevel.BackRequested += TopLevelBackRequested;
_pointerDisposable = Disposable.Create(() =>
{
topLevel.PointerReleased -= PointerReleasedOutside;
topLevel.BackRequested -= TopLevelBackRequested;
});
topLevel.PointerReleased += PointerReleasedOutside;
topLevel.BackRequested += TopLevelBackRequested;
}
}
}

10
src/Avalonia.Controls/Utils/BindingEvaluator.cs

@ -19,6 +19,15 @@ internal sealed class BindingEvaluator<T> : StyledElement, IDisposable
public static readonly StyledProperty<T> ValueProperty =
AvaloniaProperty.Register<BindingEvaluator<T>, T>("Value");
/// <summary>
/// Gets or sets the data item value.
/// </summary>
public T Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public T Evaluate(object? dataContext)
{
// Only update the DataContext if necessary
@ -49,6 +58,7 @@ internal sealed class BindingEvaluator<T> : StyledElement, IDisposable
DataContext = null;
}
[return: NotNullIfNotNull(nameof(binding))]
public static BindingEvaluator<T>? TryCreate(IBinding? binding)
{
if (binding is null)

12
src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs

@ -132,18 +132,22 @@ namespace Avalonia.Controls.Utils
}
}
var l = Listeners.ToArray();
if (Dispatcher.UIThread.CheckAccess())
{
var l = Listeners.ToArray();
Notify(_collection, e, l);
}
else
{
var eCapture = e;
Dispatcher.UIThread.Post(() => Notify(_collection, eCapture, l), DispatcherPriority.Send);
Dispatcher.UIThread.Post(() =>
{
var l = Listeners.ToArray();
Notify(_collection, eCapture, l);
},
DispatcherPriority.Send);
}
}
}
}
}
}

209
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -8,6 +8,7 @@ using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Reactive;
using Avalonia.Utilities;
using Avalonia.VisualTree;
@ -51,6 +52,12 @@ namespace Avalonia.Controls
RoutedEvent.Register<VirtualizingStackPanel, RoutedEventArgs>(
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="CacheLength"/> property.
/// </summary>
public static readonly StyledProperty<double> CacheLengthProperty =
AvaloniaProperty.Register<VirtualizingStackPanel, double>(nameof(CacheLength), 0.0,
validate: v => v is >= 0 and <= 2);
private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
@ -73,12 +80,24 @@ namespace Avalonia.Controls
private int _focusedIndex = -1;
private Control? _realizingElement;
private int _realizingIndex = -1;
private double _bufferFactor;
private bool _hasReachedStart = false;
private bool _hasReachedEnd = false;
private Rect _extendedViewport;
static VirtualizingStackPanel()
{
CacheLengthProperty.Changed.AddClassHandler<VirtualizingStackPanel>((x, e) => x.OnCacheLengthChanged(e));
}
public VirtualizingStackPanel()
{
_recycleElement = RecycleElement;
_recycleElementOnItemRemoved = RecycleElementOnItemRemoved;
_updateElementIndex = UpdateElementIndex;
_bufferFactor = Math.Max(0, CacheLength);
EffectiveViewportChanged += OnEffectiveViewportChanged;
}
@ -131,6 +150,20 @@ namespace Avalonia.Controls
set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
/// <summary>
/// Gets or sets the CacheLength.
/// </summary>
/// <remarks>The factor determines how much additional space to maintain above and below the viewport.
/// A value of 0.5 means half the viewport size will be buffered on each side (up-down or left-right)
/// This uses more memory as more UI elements are realized, but greatly reduces the number of Measure-Arrange
/// cycles which can cause heavy GC pressure depending on the complexity of the item layouts.
/// </remarks>
public double CacheLength
{
get => GetValue(CacheLengthProperty);
set => SetValue(CacheLengthProperty, value);
}
/// <summary>
/// Gets the index of the first realized element, or -1 if no elements are realized.
/// </summary>
@ -141,6 +174,16 @@ namespace Avalonia.Controls
/// </summary>
public int LastRealizedIndex => _realizedElements?.LastIndex ?? -1;
/// <summary>
/// Returns the viewport that contains any visible elements
/// </summary>
internal Rect ViewPort => _viewport;
/// <summary>
/// Returns the extended viewport that contains any visible elements and the additional elements for fast scrolling (viewport * CacheLength * 2)
/// </summary>
internal Rect ExtendedViewPort => _extendedViewport;
protected override Size MeasureOverride(Size availableSize)
{
var items = Items;
@ -217,8 +260,12 @@ namespace Avalonia.Controls
var rect = orientation == Orientation.Horizontal ?
new Rect(u, 0, sizeU, finalSize.Height) :
new Rect(0, u, finalSize.Width, sizeU);
e.Arrange(rect);
_scrollAnchorProvider?.RegisterAnchorCandidate(e);
if (_viewport.Intersects(rect))
_scrollAnchorProvider?.RegisterAnchorCandidate(e);
u += orientation == Orientation.Horizontal ? rect.Width : rect.Height;
}
}
@ -230,6 +277,7 @@ namespace Avalonia.Controls
var rect = orientation == Orientation.Horizontal ?
new Rect(u, 0, _focusedElement.DesiredSize.Width, finalSize.Height) :
new Rect(0, u, finalSize.Width, _focusedElement.DesiredSize.Height);
_focusedElement.Arrange(rect);
}
@ -416,6 +464,7 @@ namespace Avalonia.Controls
// Create and measure the element to be brought into view. Store it in a field so that
// it can be re-used in the layout pass.
var scrollToElement = GetOrCreateElement(items, index);
scrollToElement.Measure(Size.Infinity);
// Get the expected position of the element and put it in place.
@ -483,7 +532,8 @@ namespace Avalonia.Controls
{
Debug.Assert(_realizedElements is not null);
var viewport = _viewport;
// Use the extended viewport for calculations
var viewport = _extendedViewport;
// Get the viewport in the orientation direction.
var viewportStart = orientation == Orientation.Horizontal ? viewport.X : viewport.Y;
@ -653,7 +703,6 @@ namespace Avalonia.Controls
return index * estimatedSize;
}
private void RealizeElements(
IReadOnlyList<object?> items,
Size availableSize,
@ -666,6 +715,10 @@ namespace Avalonia.Controls
var index = viewport.anchorIndex;
var horizontal = Orientation == Orientation.Horizontal;
var u = viewport.anchorU;
// Reset boundary flags
_hasReachedStart = false;
_hasReachedEnd = false;
// If the anchor element is at the beginning of, or before, the start of the viewport
// then we can recycle all elements before it.
@ -678,8 +731,9 @@ namespace Avalonia.Controls
_realizingIndex = index;
var e = GetOrCreateElement(items, index);
_realizingElement = e;
e.Measure(availableSize);
var sizeU = horizontal ? e.DesiredSize.Width : e.DesiredSize.Height;
var sizeV = horizontal ? e.DesiredSize.Height : e.DesiredSize.Width;
@ -691,7 +745,10 @@ namespace Avalonia.Controls
_realizingIndex = -1;
_realizingElement = null;
} while (u < viewport.viewportUEnd && index < items.Count);
// Check if we reached the end of the collection
_hasReachedEnd = index >= items.Count;
// Store the last index and end U position for the desired size calculation.
viewport.lastIndex = index - 1;
viewport.realizedEndU = u;
@ -706,8 +763,8 @@ namespace Avalonia.Controls
while (u > viewport.viewportUStart && index >= 0)
{
var e = GetOrCreateElement(items, index);
e.Measure(availableSize);
var sizeU = horizontal ? e.DesiredSize.Width : e.DesiredSize.Height;
var sizeV = horizontal ? e.DesiredSize.Height : e.DesiredSize.Width;
u -= sizeU;
@ -716,6 +773,9 @@ namespace Avalonia.Controls
viewport.measuredV = Math.Max(viewport.measuredV, sizeV);
--index;
}
// Check if we reached the start of the collection
_hasReachedStart = index < 0;
// We can now recycle elements before the first element.
_realizedElements.RecycleElementsBefore(index + 1, _recycleElement);
@ -748,7 +808,7 @@ namespace Avalonia.Controls
{
return _realizedElements?.GetElement(index);
}
private static Control? GetRealizedElement(
int index,
ref int specialIndex,
@ -891,22 +951,146 @@ namespace Avalonia.Controls
ItemContainerGenerator.ItemContainerIndexChanged(element, oldIndex, newIndex);
}
private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e)
{
var vertical = Orientation == Orientation.Vertical;
var oldViewportStart = vertical ? _viewport.Top : _viewport.Left;
var oldViewportEnd = vertical ? _viewport.Bottom : _viewport.Right;
var oldExtendedViewportStart = vertical ? _extendedViewport.Top : _extendedViewport.Left;
var oldExtendedViewportEnd = vertical ? _extendedViewport.Bottom : _extendedViewport.Right;
// Update current viewport
_viewport = e.EffectiveViewport.Intersect(new(Bounds.Size));
_isWaitingForViewportUpdate = false;
// Calculate buffer sizes based on viewport dimensions
var viewportSize = vertical ? _viewport.Height : _viewport.Width;
var bufferSize = viewportSize * _bufferFactor;
// Calculate extended viewport with relative buffers
var extendedViewportStart = vertical ?
Math.Max(0, _viewport.Top - bufferSize) :
Math.Max(0, _viewport.Left - bufferSize);
var extendedViewportEnd = vertical ?
Math.Min(Bounds.Height, _viewport.Bottom + bufferSize) :
Math.Min(Bounds.Width, _viewport.Right + bufferSize);
// special case:
// If we are at the start of the list, append 2 * CacheLength additional items
// If we are at the end of the list, prepend 2 * CacheLength additional items
// - this way we always maintain "2 * CacheLength * element" items.
if (vertical)
{
var spaceAbove = _viewport.Top - bufferSize;
var spaceBelow = Bounds.Height - (_viewport.Bottom + bufferSize);
if (spaceAbove < 0 && spaceBelow >= 0)
extendedViewportEnd = Math.Min(Bounds.Height, extendedViewportEnd + Math.Abs(spaceAbove));
if (spaceAbove >= 0 && spaceBelow < 0)
extendedViewportStart = Math.Max(0, extendedViewportStart - Math.Abs(spaceBelow));
}
else
{
var spaceLeft = _viewport.Left - bufferSize;
var spaceRight = Bounds.Width - (_viewport.Right + bufferSize);
if (spaceLeft < 0 && spaceRight >= 0)
extendedViewportEnd = Math.Min(Bounds.Width, extendedViewportEnd + Math.Abs(spaceLeft));
if(spaceLeft >= 0 && spaceRight < 0)
extendedViewportStart = Math.Max(0, extendedViewportStart - Math.Abs(spaceRight));
}
Rect extendedViewPort;
if (vertical)
{
extendedViewPort = new Rect(
_viewport.X,
extendedViewportStart,
_viewport.Width,
extendedViewportEnd - extendedViewportStart);
}
else
{
extendedViewPort = new Rect(
extendedViewportStart,
_viewport.Y,
extendedViewportEnd - extendedViewportStart,
_viewport.Height);
}
// Determine if we need a new measure
var newViewportStart = vertical ? _viewport.Top : _viewport.Left;
var newViewportEnd = vertical ? _viewport.Bottom : _viewport.Right;
var newExtendedViewportStart = vertical ? extendedViewPort.Top : extendedViewPort.Left;
var newExtendedViewportEnd = vertical ? extendedViewPort.Bottom : extendedViewPort.Right;
var needsMeasure = false;
// Case 1: Viewport has changed significantly
if (!MathUtilities.AreClose(oldViewportStart, newViewportStart) ||
!MathUtilities.AreClose(oldViewportEnd, newViewportEnd))
{
// Case 1a: The new viewport exceeds the old extended viewport
if (newViewportStart < oldExtendedViewportStart ||
newViewportEnd > oldExtendedViewportEnd)
{
needsMeasure = true;
}
// Case 1b: The extended viewport has changed significantly
else if (!MathUtilities.AreClose(oldExtendedViewportStart, newExtendedViewportStart) ||
!MathUtilities.AreClose(oldExtendedViewportEnd, newExtendedViewportEnd))
{
// Check if we're about to scroll into an area where we don't have realized elements
// This would be the case if we're near the edge of our current extended viewport
var nearingEdge = false;
if (_realizedElements != null)
{
var firstRealizedElementU = _realizedElements.StartU;
var lastRealizedElementU = _realizedElements.StartU;
for (var i = 0; i < _realizedElements.Count; i++)
{
lastRealizedElementU += _realizedElements.SizeU[i];
}
// If scrolling up/left and nearing the top/left edge of realized elements
if (newViewportStart < oldViewportStart &&
newViewportStart - newExtendedViewportStart < bufferSize)
{
// Edge case: We're at item 0 with excess measurement space.
// Skip re-measuring since we're at the list start and it won't change the result.
// This prevents redundant Measure-Arrange cycles when at list beginning.
nearingEdge = !_hasReachedStart;
}
// If scrolling down/right and nearing the bottom/right edge of realized elements
if (newViewportEnd > oldViewportEnd &&
newExtendedViewportEnd - newViewportEnd < bufferSize)
{
// Edge case: We're at the last item with excess measurement space.
// Skip re-measuring since we're at the list end and it won't change the result.
// This prevents redundant Measure-Arrange cycles when at list beginning.
nearingEdge = !_hasReachedEnd;
}
}
else
{
nearingEdge = true;
}
needsMeasure = nearingEdge;
}
}
if (needsMeasure)
{
// only store the new "old" extended viewport if we _did_ actually measure
_extendedViewport = extendedViewPort;
InvalidateMeasure();
}
}
@ -924,6 +1108,15 @@ namespace Avalonia.Controls
}
}
private void OnCacheLengthChanged(AvaloniaPropertyChangedEventArgs e)
{
var newValue = e.GetNewValue<double>();
_bufferFactor = newValue;
// Force a recalculation of the extended viewport on the next layout pass
InvalidateMeasure();
}
/// <inheritdoc/>
public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
{

70
src/Avalonia.Controls/Window.cs

@ -181,9 +181,24 @@ namespace Avalonia.Controls
public static readonly StyledProperty<WindowStartupLocation> WindowStartupLocationProperty =
AvaloniaProperty.Register<Window, WindowStartupLocation>(nameof(WindowStartupLocation));
/// <summary>
/// Defines the <see cref="CanResize"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanResizeProperty =
AvaloniaProperty.Register<Window, bool>(nameof(CanResize), true);
/// <summary>
/// Defines the <see cref="CanMinimize"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanMinimizeProperty =
AvaloniaProperty.Register<Window, bool>(nameof(CanMinimize), true);
/// <summary>
/// Defines the <see cref="CanMaximize"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanMaximizeProperty =
AvaloniaProperty.Register<Window, bool>(nameof(CanMaximize), true, coerce: CoerceCanMaximize);
/// <summary>
/// Routed event that can be used for global tracking of window destruction
/// </summary>
@ -236,6 +251,8 @@ namespace Avalonia.Controls
CreatePlatformImplBinding(TitleProperty, title => PlatformImpl!.SetTitle(title));
CreatePlatformImplBinding(IconProperty, icon => PlatformImpl!.SetIcon((icon ?? s_defaultIcon.Value)?.PlatformImpl));
CreatePlatformImplBinding(CanResizeProperty, canResize => PlatformImpl!.CanResize(canResize));
CreatePlatformImplBinding(CanMinimizeProperty, canMinimize => PlatformImpl!.SetCanMinimize(canMinimize));
CreatePlatformImplBinding(CanMaximizeProperty, canMaximize => PlatformImpl!.SetCanMaximize(canMaximize));
CreatePlatformImplBinding(ShowInTaskbarProperty, show => PlatformImpl!.ShowTaskbarIcon(show));
CreatePlatformImplBinding(WindowStateProperty, state => PlatformImpl!.WindowState = state);
@ -407,6 +424,32 @@ namespace Avalonia.Controls
set => SetValue(CanResizeProperty, value);
}
/// <summary>
/// Enables or disables minimizing the window.
/// </summary>
/// <remarks>
/// This property might be ignored by some window managers on Linux.
/// </remarks>
public bool CanMinimize
{
get => GetValue(CanMinimizeProperty);
set => SetValue(CanMinimizeProperty, value);
}
/// <summary>
/// Enables or disables maximizing the window.
/// </summary>
/// <remarks>
/// <para>When <see cref="CanResize"/> is false, this property is always false.</para>
/// <para>On macOS, setting this property to false also disables the full screen mode.</para>
/// <para>This property might be ignored by some window managers on Linux.</para>
/// </remarks>
public bool CanMaximize
{
get => GetValue(CanMaximizeProperty);
set => SetValue(CanMaximizeProperty, value);
}
/// <summary>
/// Gets or sets the icon of the window.
/// </summary>
@ -438,6 +481,11 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Gets whether this window was opened as a dialog
/// </summary>
public bool IsDialog => _showingAsDialog;
/// <summary>
/// Starts moving a window with left button being held. Should be called from left mouse button press event handler
/// </summary>
@ -718,12 +766,12 @@ namespace Avalonia.Controls
return null;
}
_showingAsDialog = modal;
RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
EnsureInitialized();
ApplyStyling();
_shown = true;
_showingAsDialog = modal;
IsVisible = true;
// If window position was not set before then platform may provide incorrect scaling at this time,
@ -941,7 +989,7 @@ namespace Avalonia.Controls
{
return;
}
var location = GetEffectiveWindowStartupLocation(owner);
switch (location)
@ -974,7 +1022,7 @@ namespace Avalonia.Controls
return startupLocation;
}
private void SetWindowStartupLocation(Window? owner = null)
{
if (_wasShownBefore)
@ -1009,7 +1057,7 @@ namespace Avalonia.Controls
screen ??= Screens.ScreenFromPoint(Position);
screen ??= Screens.Primary;
if (screen is not null)
{
var childRect = screen.WorkingArea.CenterRect(rect);
@ -1029,7 +1077,7 @@ namespace Avalonia.Controls
var childRect = ownerRect.CenterRect(rect);
var screen = Screens.ScreenFromWindow(owner);
childRect = ApplyScreenConstraint(screen, childRect);
Position = childRect.Position;
@ -1037,7 +1085,7 @@ namespace Avalonia.Controls
if (!_positionWasSet && DesktopScaling != PlatformImpl?.DesktopScaling) // Platform returns incorrect scaling, forcing setting position may fix it
PlatformImpl?.Move(Position);
PixelRect ApplyScreenConstraint(Screen? screen, PixelRect childRect)
{
if (screen?.WorkingArea is { } constraint)
@ -1184,7 +1232,7 @@ namespace Avalonia.Controls
PlatformImpl?.SetSystemDecorations(typedNewValue);
}
if (change.Property == OwnerProperty)
else if (change.Property == OwnerProperty)
{
var oldParent = change.OldValue as Window;
var newParent = change.NewValue as Window;
@ -1197,6 +1245,11 @@ namespace Avalonia.Controls
impl.SetParent(_showingAsDialog ? newParent?.PlatformImpl! : (newParent?.PlatformImpl ?? null));
}
}
else if (change.Property == CanResizeProperty)
{
CoerceValue(CanMaximizeProperty);
}
}
protected override AutomationPeer OnCreateAutomationPeer()
@ -1217,5 +1270,8 @@ namespace Avalonia.Controls
}
return null;
}
private static bool CoerceCanMaximize(AvaloniaObject target, bool value)
=> value && target is not Window { CanResize: false };
}
}

8
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -133,6 +133,14 @@ namespace Avalonia.DesignerSupport.Remote
{
}
public void SetCanMinimize(bool value)
{
}
public void SetCanMaximize(bool value)
{
}
public void SetTopmost(bool value)
{
}

8
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -152,6 +152,14 @@ namespace Avalonia.DesignerSupport.Remote
{
}
public void SetCanMinimize(bool value)
{
}
public void SetCanMaximize(bool value)
{
}
public void SetTopmost(bool value)
{
}

25
src/Avalonia.Native/WindowImpl.cs

@ -18,6 +18,7 @@ namespace Avalonia.Native
private DoubleClickHelper _doubleClickHelper;
private readonly ITopLevelNativeMenuExporter _nativeMenuExporter;
private bool _canResize = true;
private bool _canMaximize = true;
internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(factory)
{
@ -77,6 +78,17 @@ namespace Avalonia.Native
_native.SetCanResize(value.AsComBool());
}
public void SetCanMinimize(bool value)
{
_native.SetCanMinimize(value.AsComBool());
}
public void SetCanMaximize(bool value)
{
_canMaximize = value;
_native.SetCanMaximize(value.AsComBool());
}
public void SetSystemDecorations(Controls.SystemDecorations enabled)
{
_native.SetDecorations((Interop.SystemDecorations)enabled);
@ -138,10 +150,17 @@ namespace Avalonia.Native
{
if (_doubleClickHelper.IsDoubleClick(e.Timestamp, e.Position))
{
if (_canResize)
switch (WindowState)
{
WindowState = WindowState is WindowState.Maximized or WindowState.FullScreen ?
WindowState.Normal : WindowState.Maximized;
case WindowState.Maximized or WindowState.FullScreen
when _canResize:
WindowState = WindowState.Normal;
break;
case WindowState.Normal
when _canMaximize:
WindowState = WindowState.Maximized;
break;
}
}
else

2
src/Avalonia.Native/avn.idl

@ -772,6 +772,8 @@ interface IAvnWindow : IAvnWindowBase
{
HRESULT SetEnabled(bool enable);
HRESULT SetCanResize(bool value);
HRESULT SetCanMinimize(bool value);
HRESULT SetCanMaximize(bool value);
HRESULT SetDecorations(SystemDecorations value);
HRESULT SetTitle(char* utf8Title);
HRESULT SetTitleBarColor(AvnColor color);

9
src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml

@ -102,6 +102,15 @@
<StaticResource x:Key="ToggleButtonBorderBrushIndeterminateDisabled"
ResourceKey="SystemControlDisabledTransparentBrush" />
<!-- GroupBox -->
<Thickness x:Key="GroupBoxPadding">4</Thickness>
<x:Double x:Key="GroupBoxHeaderFontSize">16</x:Double>
<Thickness x:Key="GroupBoxHeaderMargin">0,4,0,12</Thickness>
<Thickness x:Key="GroupBoxBorderThickness">1</Thickness>
<SolidColorBrush x:Key="GroupBoxBackground" Color="Transparent" />
<StaticResource x:Key="GroupBoxBorderBrush" ResourceKey="SystemControlForegroundBaseMediumBrush" />
<StaticResource x:Key="GroupBoxHeaderForeground" ResourceKey="SystemBaseHighColor" />
<!-- Resources for HyperlinkButton.xaml -->
<Color x:Key="HyperlinkVisitedColor">#FF681DA8</Color>
<SolidColorBrush x:Key="HyperlinkVisitedBrush" Color="{StaticResource HyperlinkVisitedColor}" />

1
src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml

@ -42,6 +42,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
CaretIndex="{TemplateBinding CaretIndex, Mode=TwoWay}"
ClearSelectionOnLostFocus="{TemplateBinding ClearSelectionOnLostFocus}"
Padding="{TemplateBinding Padding}"
Margin="0"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"

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

@ -112,7 +112,7 @@
<Style Selector="^:fullscreen /template/ Button#PART_MinimizeButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^ /template/ Button#PART_RestoreButton:disabled">
<Style Selector="^ /template/ Button:disabled">
<Setter Property="Opacity" Value="0.2"/>
</Style>
</ControlTheme>

56
src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

@ -16,6 +16,7 @@
<ComboBoxItem>Item 1</ComboBoxItem>
<ComboBoxItem>Item 2</ComboBoxItem>
</ComboBox>
<ComboBox PlaceholderText="Error">
<DataValidationErrors.Error>
<sys:Exception>
@ -25,6 +26,25 @@
</sys:Exception>
</DataValidationErrors.Error>
</ComboBox>
<ComboBox SelectedIndex="1" IsEditable="True">
<ComboBoxItem>Item A</ComboBoxItem>
<ComboBoxItem>Item b</ComboBoxItem>
<ComboBoxItem>Item c</ComboBoxItem>
</ComboBox>
<ComboBox SelectedIndex="0">
<ComboBox.SelectionBoxItemTemplate>
<DataTemplate>
<Border Padding="20" BorderBrush="Red" BorderThickness="1">
<TextBlock Text="{ReflectionBinding}"/>
</Border>
</DataTemplate>
</ComboBox.SelectionBoxItemTemplate>
<ComboBoxItem>Item A</ComboBoxItem>
<ComboBoxItem>Item b</ComboBoxItem>
<ComboBoxItem>Item c</ComboBoxItem>
</ComboBox>
</StackPanel>
</Border>
</Design.PreviewWith>
@ -80,17 +100,42 @@
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}"
Text="{TemplateBinding PlaceholderText}"
Foreground="{TemplateBinding PlaceholderForeground}"
IsVisible="{TemplateBinding SelectionBoxItem, Converter={x:Static ObjectConverters.IsNull}}" />
Foreground="{TemplateBinding PlaceholderForeground}">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SelectionBoxItem" RelativeSource="{RelativeSource TemplatedParent}" Converter="{x:Static ObjectConverters.IsNull}" />
<Binding Path="!IsEditable" RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<ContentControl x:Name="ContentPresenter"
Content="{TemplateBinding SelectionBoxItem}"
Grid.Column="0"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
IsVisible="{TemplateBinding IsEditable, Converter={x:Static BoolConverters.Not}}">
</ContentControl>
<TextBox Name="PART_EditableTextBox"
Grid.Column="0"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Foreground="{TemplateBinding Foreground}"
Background="Transparent"
Text="{TemplateBinding Text, Mode=TwoWay}"
Watermark="{TemplateBinding PlaceholderText}"
BorderThickness="0"
IsVisible="{TemplateBinding IsEditable}">
<TextBox.Resources>
<SolidColorBrush x:Key="TextControlBackgroundFocused">Transparent</SolidColorBrush>
<SolidColorBrush x:Key="TextControlBackgroundPointerOver">Transparent</SolidColorBrush>
<Thickness x:Key="TextControlBorderThemeThicknessFocused">0</Thickness>
</TextBox.Resources>
</TextBox>
<Border x:Name="DropDownOverlay"
Grid.Column="1"
Background="Transparent"
@ -203,6 +248,11 @@
<Setter Property="Foreground" Value="{DynamicResource ComboBoxDropDownGlyphForegroundDisabled}" />
</Style>
</Style>
<Style Selector="^[IsEditable=true]">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="KeyboardNavigation.TabNavigation" Value="Local" />
</Style>
</ControlTheme>
</ResourceDictionary>

23
src/Avalonia.Themes.Fluent/Controls/GroupBox.xaml

@ -33,16 +33,19 @@
CornerRadius="{TemplateBinding CornerRadius}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<Border.Clip>
<CombinedGeometry GeometryCombineMode="Exclude">
<CombinedGeometry.Geometry1>
<RectangleGeometry Rect="{Binding #RootGrid.Bounds}" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<RectangleGeometry Rect="{Binding #Header.Bounds}" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Border.Clip>
<Border.OpacityMask>
<VisualBrush>
<VisualBrush.Visual>
<Canvas Background="Transparent">
<Rectangle Fill="Transparent"
Width="{Binding #Header.Bounds.Width}"
Height="{Binding #Header.Bounds.Height}"
Canvas.Left="{Binding #Header.Bounds.X}"
Canvas.Top="{Binding #Header.Bounds.Y}"/>
</Canvas>
</VisualBrush.Visual>
</VisualBrush>
</Border.OpacityMask>
</Border>
<!-- ContentPresenter for the header -->

1
src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml

@ -17,6 +17,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
CaretIndex="{TemplateBinding CaretIndex, Mode=TwoWay}"
ClearSelectionOnLostFocus="{TemplateBinding ClearSelectionOnLostFocus}"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
Watermark="{TemplateBinding Watermark}"
MaxLength="{TemplateBinding MaxLength}"

37
src/Avalonia.Themes.Simple/Controls/ComboBox.xaml

@ -27,15 +27,37 @@
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Foreground="{TemplateBinding PlaceholderForeground}"
IsVisible="{TemplateBinding SelectionBoxItem,
Converter={x:Static ObjectConverters.IsNull}}"
Text="{TemplateBinding PlaceholderText}" />
Text="{TemplateBinding PlaceholderText}">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SelectionBoxItem" RelativeSource="{RelativeSource TemplatedParent}" Converter="{x:Static ObjectConverters.IsNull}" />
<Binding Path="!IsEditable" RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<ContentControl Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}">
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
IsVisible="{TemplateBinding IsEditable, Converter={x:Static BoolConverters.Not}}">
</ContentControl>
<TextBox Name="PART_EditableTextBox"
Grid.Column="0"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Foreground="{TemplateBinding Foreground}"
Background="Transparent"
Text="{TemplateBinding Text, Mode=TwoWay}"
BorderThickness="0"
IsVisible="{TemplateBinding IsEditable}">
<TextBox.Resources>
<SolidColorBrush x:Key="TextControlBackgroundFocused">Transparent</SolidColorBrush>
<SolidColorBrush x:Key="TextControlBackgroundPointerOver">Transparent</SolidColorBrush>
<Thickness x:Key="TextControlBorderThemeThicknessFocused">0</Thickness>
</TextBox.Resources>
</TextBox>
<ToggleButton Name="toggle"
Grid.Column="1"
Background="Transparent"
@ -75,11 +97,18 @@
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ Border#border">
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}" />
</Style>
<Style Selector="^:disabled /template/ Border#border">
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
</Style>
<Style Selector="^[IsEditable=true]">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="KeyboardNavigation.TabNavigation" Value="Local" />
</Style>
</ControlTheme>
</ResourceDictionary>

23
src/Avalonia.Themes.Simple/Controls/GroupBox.xaml

@ -31,16 +31,19 @@
CornerRadius="{TemplateBinding CornerRadius}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<Border.Clip>
<CombinedGeometry GeometryCombineMode="Exclude">
<CombinedGeometry.Geometry1>
<RectangleGeometry Rect="{Binding #RootGrid.Bounds}" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<RectangleGeometry Rect="{Binding #Header.Bounds}" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Border.Clip>
<Border.OpacityMask>
<VisualBrush>
<VisualBrush.Visual>
<Canvas Background="Transparent">
<Rectangle Fill="Transparent"
Width="{Binding #Header.Bounds.Width}"
Height="{Binding #Header.Bounds.Height}"
Canvas.Left="{Binding #Header.Bounds.X}"
Canvas.Top="{Binding #Header.Bounds.Y}"/>
</Canvas>
</VisualBrush.Visual>
</VisualBrush>
</Border.OpacityMask>
</Border>
<!-- ContentPresenter for the header -->

42
src/Avalonia.X11/X11Window.cs

@ -316,22 +316,28 @@ namespace Avalonia.X11
|| _systemDecorations == SystemDecorations.None)
decorations = 0;
if (!_canResize || !IsEnabled)
var isDisabled = !IsEnabled;
if (!_canResize || isDisabled)
{
functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize);
decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH);
functions &= ~MotifFunctions.Resize;
decorations &= ~MotifDecorations.ResizeH;
}
if (!IsEnabled)
{
functions &= ~(MotifFunctions.Resize | MotifFunctions.Minimize);
UpdateSizeHints(null, true);
if (!_canMinimize || isDisabled)
{
functions &= ~MotifFunctions.Minimize;
decorations &= ~MotifDecorations.Minimize;
}
else
if (!_canMaximize || isDisabled)
{
UpdateSizeHints(null);
functions &= ~MotifFunctions.Maximize;
decorations &= ~MotifDecorations.Maximize;
}
UpdateSizeHints(null, isDisabled);
var hints = new MotifWmHints
{
flags = new IntPtr((int)(MotifFlags.Decorations | MotifFlags.Functions)),
@ -857,6 +863,8 @@ namespace Avalonia.X11
private SystemDecorations _systemDecorations = SystemDecorations.Full;
private bool _canResize = true;
private bool _canMinimize = true;
private bool _canMaximize = true;
private const int MaxWindowDimension = 100000;
private (Size minSize, Size maxSize) _scaledMinMaxSize =
@ -1162,6 +1170,18 @@ namespace Avalonia.X11
UpdateSizeHints(null);
}
public void SetCanMinimize(bool value)
{
_canMinimize = value;
UpdateMotifHints();
}
public void SetCanMaximize(bool value)
{
_canMaximize = value;
UpdateMotifHints();
}
public void SetCursor(ICursorImpl? cursor)
{
if (cursor == null)
@ -1245,10 +1265,10 @@ namespace Avalonia.X11
ptr1 = l0,
ptr2 = l1 ?? IntPtr.Zero,
ptr3 = l2 ?? IntPtr.Zero,
ptr4 = l3 ?? IntPtr.Zero
ptr4 = l3 ?? IntPtr.Zero,
ptr5 = l4 ?? IntPtr.Zero
}
};
xev.ClientMessageEvent.ptr4 = l4 ?? IntPtr.Zero;
XSendEvent(_x11.Display, _x11.RootWindow, false,
new IntPtr((int)(EventMask.SubstructureRedirectMask | EventMask.SubstructureNotifyMask)), ref xev);

8
src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs

@ -165,6 +165,14 @@ namespace Avalonia.Headless
}
public void SetCanMinimize(bool value)
{
}
public void SetCanMaximize(bool value)
{
}
public Func<WindowCloseReason, bool>? Closing { get; set; }
private class FramebufferProxy : ILockedFramebuffer

2
src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs

@ -12,6 +12,8 @@ namespace Avalonia.Win32
{
ShowInTaskbar = false,
IsResizable = false,
IsMinimizable = false,
IsMaximizable = false,
Decorations = SystemDecorations.None
};
}

15
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -1502,6 +1502,21 @@ namespace Avalonia.Win32.Interop
[DllImport("shell32", CharSet = CharSet.Auto)]
public static extern int Shell_NotifyIcon(NIM dwMessage, NOTIFYICONDATA lpData);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool ChangeWindowMessageFilterEx(
IntPtr hWnd,
uint message,
MessageFilterFlag action,
IntPtr pChangeFilterStruct
);
public enum MessageFilterFlag
{
MSGFLT_RESET = 0,
MSGFLT_ALLOW = 1,
MSGFLT_DISALLOW = 2,
}
[DllImport("shell32", CharSet = CharSet.Auto)]
public static extern nint SHAppBarMessage(AppBarMessage dwMessage, ref APPBARDATA lpData);

2
src/Windows/Avalonia.Win32/PopupImpl.cs

@ -111,6 +111,8 @@ namespace Avalonia.Win32
{
ShowInTaskbar = false,
IsResizable = false,
IsMinimizable = false,
IsMaximizable = false,
Decorations = SystemDecorations.None,
};

7
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@ -37,7 +37,12 @@ namespace Avalonia.Win32
new PixelSize(32, 32), new Vector(96, 96), PixelFormats.Bgra8888, AlphaFormat.Unpremul);
s_emptyIcon = new Win32Icon(bitmap);
}
internal static void ChangeWindowMessageFilter(IntPtr hWnd)
{
ChangeWindowMessageFilterEx(hWnd, WM_TASKBARCREATED, MessageFilterFlag.MSGFLT_ALLOW, IntPtr.Zero);
}
public TrayIconImpl()
{
FindTaskBarMonitor();

2
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -218,6 +218,8 @@ namespace Avalonia.Win32
{
throw new Win32Exception();
}
TrayIconImpl.ChangeWindowMessageFilter(_hwnd);
}
public ITrayIconImpl CreateTrayIcon()

36
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -136,6 +136,8 @@ namespace Avalonia.Win32
{
ShowInTaskbar = false,
IsResizable = true,
IsMinimizable = true,
IsMaximizable = true,
Decorations = SystemDecorations.Full
};
@ -858,6 +860,24 @@ namespace Avalonia.Win32
UpdateWindowProperties(newWindowProperties);
}
public void SetCanMinimize(bool value)
{
var newWindowProperties = _windowProperties;
newWindowProperties.IsMinimizable = value;
UpdateWindowProperties(newWindowProperties);
}
public void SetCanMaximize(bool value)
{
var newWindowProperties = _windowProperties;
newWindowProperties.IsMaximizable = value;
UpdateWindowProperties(newWindowProperties);
}
public void SetSystemDecorations(SystemDecorations value)
{
var newWindowProperties = _windowProperties;
@ -1425,15 +1445,19 @@ namespace Avalonia.Win32
style |= WindowStyles.WS_VISIBLE;
if (newProperties.IsResizable || newProperties.WindowState == WindowState.Maximized)
{
style |= WindowStyles.WS_THICKFRAME;
style |= WindowStyles.WS_MAXIMIZEBOX;
}
else
{
style &= ~WindowStyles.WS_THICKFRAME;
if (newProperties.IsMinimizable)
style |= WindowStyles.WS_MINIMIZEBOX;
else
style &= ~WindowStyles.WS_MINIMIZEBOX;
if (newProperties.IsMaximizable || (newProperties.WindowState == WindowState.Maximized && newProperties.IsResizable))
style |= WindowStyles.WS_MAXIMIZEBOX;
else
style &= ~WindowStyles.WS_MAXIMIZEBOX;
}
const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU | WindowStyles.WS_BORDER;
@ -1656,6 +1680,8 @@ namespace Avalonia.Win32
{
public bool ShowInTaskbar;
public bool IsResizable;
public bool IsMinimizable;
public bool IsMaximizable;
public SystemDecorations Decorations;
public bool IsFullScreen;
public WindowState WindowState;

3
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(AvsCurrentIOSTargetFramework);$(AvsCurrentTvOSTargetFramework)</TargetFrameworks>
<TargetFrameworks>$(AvsCurrentMacCatalystTargetFramework);$(AvsCurrentIOSTargetFramework);$(AvsCurrentTvOSTargetFramework)</TargetFrameworks>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">$(AvsMinSupportedIOSVersion)</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tvos'">$(AvsMinSupportedTvOSVersion)</SupportedOSPlatformVersion>
<!-- Not yet enabled as a target framework -->
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">$(AvsMinSupportedMacCatalystVersion)</SupportedOSPlatformVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

70
src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs

@ -26,7 +26,7 @@ internal class IOSStorageProvider : IStorageProvider
public bool CanOpen => true;
public bool CanSave => false;
public bool CanSave => true;
public bool CanPickFolder => true;
@ -161,10 +161,72 @@ internal class IOSStorageProvider : IStorageProvider
return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(uri, wellKnownFolder));
}
public Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
return Task.FromException<IStorageFile?>(
new PlatformNotSupportedException("Save file picker is not supported by iOS"));
/*
This requires a bit of dialog here...
To save a file, we need to present the user with a document picker
This requires a temp file to be created and used to "export" the file to.
When the user picks the file location and name, UIDocumentPickerViewController
will give back the URI to the real file location, which we can then use
to give back as an IStorageFile.
https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller
Yes, it is weird, but without the temp file it will explode.
*/
// Create a temporary file to use with the document picker
var tempFileName = StorageProviderHelpers.NameWithExtension(
options.SuggestedFileName ?? "document",
options.DefaultExtension,
options.FileTypeChoices?.FirstOrDefault());
var tempDir = NSFileManager.DefaultManager.GetTemporaryDirectory().Append(Guid.NewGuid().ToString(), true);
if (tempDir == null)
{
throw new InvalidOperationException("Failed to get temporary directory for save file picker");
}
var isDirectoryCreated = NSFileManager.DefaultManager.CreateDirectory(tempDir, true, null, out var error);
if (!isDirectoryCreated)
{
throw new InvalidOperationException("Failed to create temporary directory for save file picker");
}
var tempFileUrl = tempDir.Append(tempFileName, false);
// Create an empty file at the temp location
NSData.FromBytes(0, 0).Save(tempFileUrl, false);
UIDocumentPickerViewController documentPicker;
if (OperatingSystem.IsIOSVersionAtLeast(14))
{
documentPicker = new UIDocumentPickerViewController(new[] { tempFileUrl }, asCopy: true);
}
else
{
#pragma warning disable CA1422
documentPicker = new UIDocumentPickerViewController(tempFileUrl, UIDocumentPickerMode.ExportToService);
#pragma warning restore CA1422
}
using (documentPicker)
{
if (OperatingSystem.IsIOSVersionAtLeast(13))
{
documentPicker.DirectoryUrl = GetUrlFromFolder(options.SuggestedStartLocation);
}
documentPicker.Title = options.Title;
var tcs = new TaskCompletionSource<NSUrl[]>();
documentPicker.Delegate = new PickerDelegate(urls => tcs.TrySetResult(urls));
var urls = await ShowPicker(documentPicker, tcs);
// Clean up the temporary directory
NSFileManager.DefaultManager.Remove(tempDir, out _);
return urls.FirstOrDefault() is { } url ? new IOSStorageFile(url) : null;
}
}
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)

2
src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs

@ -81,7 +81,7 @@ partial class AvaloniaView
[Export("smartInsertDeleteType")]
public UITextSmartInsertDeleteType SmartInsertDeleteType { get; set; } = UITextSmartInsertDeleteType.Default;
[Export("passwordRules")] public UITextInputPasswordRules PasswordRules { get; set; } = null!;
[Export("passwordRules")] public UITextInputPasswordRules? PasswordRules { get; set; } = null!;
public NSObject? WeakInputDelegate
{

13
src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj

@ -2,23 +2,16 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<DebugType>embedded</DebugType>
<IsPackable>true</IsPackable>
<IncludeSymbols>false</IncludeSymbols>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="3.9.0" />
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
<Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<Import Project="../../../build/TrimmingEnable.props" />
<Import Project="../../../build/NullableEnable.props" />
<Import Project="../../../build/AnalyzerProject.targets" />
</Project>

8
src/tools/Avalonia.Generators/Avalonia.Generators.csproj

@ -6,17 +6,10 @@
<DebugType>embedded</DebugType>
<IsPackable>true</IsPackable>
<IncludeSymbols>false</IncludeSymbols>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Nullable>enable</Nullable>
<XamlXSourcePath>../../../external/XamlX/src/XamlX</XamlXSourcePath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(XamlXSourcePath)/**/*.cs"
Exclude="$(XamlXSourcePath)/obj/**/*.cs;$(XamlXSourcePath)/IL/SreTypeSystem.cs"
@ -35,4 +28,5 @@
</ItemGroup>
<Import Project="../../../build/TrimmingEnable.props" />
<Import Project="../../../build/AnalyzerProject.targets" />
</Project>

3
src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs

@ -1,9 +1,8 @@
using System.Collections.Generic;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common.Domain;
internal interface ICodeGenerator
{
string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names);
string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names);
}

4
src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs

@ -1,6 +1,8 @@
using System;
namespace Avalonia.Generators.Common.Domain;
internal interface IGlobPattern
internal interface IGlobPattern : IEquatable<IGlobPattern>
{
bool Matches(string str);
}

12
src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs

@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using XamlX.Ast;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common.Domain;
@ -13,7 +15,11 @@ internal enum NamedFieldModifier
internal interface INameResolver
{
IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml);
EquatableList<ResolvedXmlName> ResolveXmlNames(XamlDocument xaml, CancellationToken cancellationToken);
ResolvedName ResolveName(IXamlType xamlType, string name, string? fieldModifier);
}
internal record ResolvedName(string TypeName, string Name, string FieldModifier);
internal record XamlXmlType(string Name, string? XmlNamespace, EquatableList<XamlXmlType> GenericArguments);
internal record ResolvedXmlName(XamlXmlType XmlType, string Name, string? FieldModifier);
internal record ResolvedName(string TypeName, string Name, string? FieldModifier);

41
src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs

@ -1,11 +1,46 @@
using System.Collections.Immutable;
using System.Threading;
using XamlX.Ast;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common.Domain;
internal interface IViewResolver
{
ResolvedView? ResolveView(string xaml);
ResolvedViewDocument? ResolveView(string xaml, CancellationToken cancellationToken);
}
internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml);
internal record ResolvedViewInfo(string ClassName, string Namespace)
{
public string FullName => $"{Namespace}.{ClassName}";
public override string ToString() => FullName;
}
internal record ResolvedViewDocument(string ClassName, string Namespace, XamlDocument Xaml)
: ResolvedViewInfo(ClassName, Namespace);
internal record ResolvedXmlView(
string ClassName,
string Namespace,
EquatableList<ResolvedXmlName> XmlNames)
: ResolvedViewInfo(ClassName, Namespace)
{
public ResolvedXmlView(ResolvedViewInfo info, EquatableList<ResolvedXmlName> xmlNames)
: this(info.ClassName, info.Namespace, xmlNames)
{
}
}
internal record ResolvedView(
string ClassName,
string Namespace,
bool IsWindow,
EquatableList<ResolvedName> Names)
: ResolvedViewInfo(ClassName, Namespace)
{
public ResolvedView(ResolvedViewInfo info, bool isWindow, EquatableList<ResolvedName> names)
: this(info.ClassName, info.Namespace, isWindow, names)
{
}
}

58
src/tools/Avalonia.Generators/Common/EquatableList.cs

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Avalonia.Generators.Common;
// https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md#pipeline-model-design
// With minor modification to use ReadOnlyCollection instead of List
internal class EquatableList<T>(IList<T> collection)
: ReadOnlyCollection<T>(collection), IEquatable<EquatableList<T>>
{
public bool Equals(EquatableList<T>? other)
{
// If the other list is null or a different size, they're not equal
if (other is null || Count != other.Count)
{
return false;
}
// Compare each pair of elements for equality
for (int i = 0; i < Count; i++)
{
if (!EqualityComparer<T>.Default.Equals(this[i], other[i]))
{
return false;
}
}
// If we got this far, the lists are equal
return true;
}
public override bool Equals(object? obj)
{
return Equals(obj as EquatableList<T>);
}
public override int GetHashCode()
{
var hash = 0;
for (var i = 0; i < Count; i++)
{
hash ^= this[i]?.GetHashCode() ?? 0;
}
return hash;
}
public static bool operator ==(EquatableList<T>? list1, EquatableList<T>? list2)
{
return ReferenceEquals(list1, list2)
|| list1 is not null && list2 is not null && list1.Equals(list2);
}
public static bool operator !=(EquatableList<T>? list1, EquatableList<T>? list2)
{
return !(list1 == list2);
}
}

7
src/tools/Avalonia.Generators/Common/GlobPattern.cs

@ -7,12 +7,19 @@ internal class GlobPattern : IGlobPattern
{
private const RegexOptions GlobOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline;
private readonly Regex _regex;
private readonly string _pattern;
public GlobPattern(string pattern)
{
_pattern = pattern;
var expression = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".") + "$";
_regex = new Regex(expression, GlobOptions);
}
public bool Matches(string str) => _regex.IsMatch(str);
public bool Equals(IGlobPattern other) => other is GlobPattern pattern && pattern._pattern == _pattern;
public override int GetHashCode() => _pattern.GetHashCode();
public override bool Equals(object? obj) => obj is GlobPattern pattern && Equals(pattern);
public override string ToString() => _pattern;
}

22
src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs

@ -4,14 +4,20 @@ using Avalonia.Generators.Common.Domain;
namespace Avalonia.Generators.Common;
internal class GlobPatternGroup : IGlobPattern
internal class GlobPatternGroup(IEnumerable<string> patterns)
: EquatableList<GlobPattern>(patterns.Select(p => new GlobPattern(p)).ToArray()), IGlobPattern
{
private readonly GlobPattern[] _patterns;
public bool Matches(string str)
{
for (var i = 0; i < Count; i++)
{
if (this[i].Matches(str))
return true;
}
return false;
}
public GlobPatternGroup(IEnumerable<string> patterns) =>
_patterns = patterns
.Select(pattern => new GlobPattern(pattern))
.ToArray();
public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str));
public bool Equals(IGlobPattern other) => other is GlobPatternGroup group && base.Equals(group);
public override string ToString() => $"[{string.Join(", ", this.Select(p => p.ToString()))}]";
}

20
src/tools/Avalonia.Generators/Common/ResolverExtensions.cs

@ -1,4 +1,4 @@
using System.Linq;
using System;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common;
@ -6,20 +6,14 @@ namespace Avalonia.Generators.Common;
internal static class ResolverExtensions
{
public static bool IsAvaloniaStyledElement(this IXamlType clrType) =>
clrType.HasStyledElementBaseType() ||
clrType.HasIStyledElementInterface();
Inherits(clrType, "Avalonia.StyledElement");
public static bool IsAvaloniaWindow(this IXamlType clrType) =>
Inherits(clrType, "Avalonia.Controls.Window");
private static bool HasStyledElementBaseType(this IXamlType clrType)
private static bool Inherits(IXamlType clrType, string metadataName)
{
// Check for the base type since IStyledElement interface is removed.
// https://github.com/AvaloniaUI/Avalonia/pull/9553
if (clrType.FullName == "Avalonia.StyledElement")
if (string.Equals(clrType.FullName, metadataName, StringComparison.Ordinal))
return true;
return clrType.BaseType != null && IsAvaloniaStyledElement(clrType.BaseType);
return clrType.BaseType is { } baseType && Inherits(baseType, metadataName);
}
private static bool HasIStyledElementInterface(this IXamlType clrType) =>
clrType.Interfaces.Any(abstraction =>
abstraction.IsInterface &&
abstraction.FullName == "Avalonia.IStyledElement");
}

65
src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs

@ -1,38 +1,56 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Avalonia.Generators.Common.Domain;
using XamlX;
using XamlX.Ast;
using XamlX.TypeSystem;
namespace Avalonia.Generators.Common;
internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
internal class XamlXNameResolver
: INameResolver, IXamlAstVisitor
{
private readonly List<ResolvedName> _items = new();
private readonly string _defaultFieldModifier;
private readonly Dictionary<string, ResolvedXmlName> _items = new();
private CancellationToken _cancellationToken;
public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal)
public EquatableList<ResolvedXmlName> ResolveXmlNames(XamlDocument xaml, CancellationToken cancellationToken)
{
_defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant();
_items.Clear();
try
{
_cancellationToken = cancellationToken;
xaml.Root.Visit(this);
xaml.Root.VisitChildren(this);
}
finally
{
_cancellationToken = CancellationToken.None;
}
return new EquatableList<ResolvedXmlName>(_items.Values.ToArray());
}
public IReadOnlyList<ResolvedName> ResolveNames(XamlDocument xaml)
public ResolvedName ResolveName(IXamlType clrType, string name, string? fieldModifier)
{
_items.Clear();
xaml.Root.Visit(this);
xaml.Root.VisitChildren(this);
return _items;
var typeName = $"{clrType.Namespace}.{clrType.Name}";
var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList();
var genericTypeName = typeAgs.Count == 0
? $"global::{typeName}"
: $"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>";
return new ResolvedName(genericTypeName, name, fieldModifier);
}
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{
_cancellationToken.ThrowIfCancellationRequested();
if (node is not XamlAstObjectNode objectNode)
return node;
var clrType = objectNode.Type.GetClrType();
if (!clrType.IsAvaloniaStyledElement())
return node;
var xamlType = (XamlAstXmlTypeReference)objectNode.Type;
foreach (var child in objectNode.Children)
{
@ -44,27 +62,24 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
propertyValueNode.Values[0] is XamlAstTextNode text)
{
var fieldModifier = TryGetFieldModifier(objectNode);
var typeName = $@"{clrType.Namespace}.{clrType.Name}";
var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList();
var genericTypeName = typeAgs.Count == 0
? $"global::{typeName}"
: $@"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>";
var resolvedName = new ResolvedName(genericTypeName, text.Text, fieldModifier);
if (_items.Contains(resolvedName))
var resolvedName = new ResolvedXmlName(ConvertType(xamlType), text.Text, fieldModifier);
if (_items.ContainsKey(text.Text))
continue;
_items.Add(resolvedName);
_items.Add(text.Text, resolvedName);
}
}
return node;
static XamlXmlType ConvertType(XamlAstXmlTypeReference type) => new(type.Name, type.XmlNamespace,
new EquatableList<XamlXmlType>(type.GenericArguments.Select(ConvertType).ToArray()));
}
void IXamlAstVisitor.Push(IXamlAstNode node) { }
void IXamlAstVisitor.Pop() { }
private string TryGetFieldModifier(XamlAstObjectNode objectNode)
private string? TryGetFieldModifier(XamlAstObjectNode objectNode)
{
// We follow Xamarin.Forms API behavior in terms of x:FieldModifier here:
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers
@ -87,7 +102,7 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
"protected" => "protected",
"internal" => "internal",
"notpublic" => "internal",
_ => _defaultFieldModifier
_ => null
};
}

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

Loading…
Cancel
Save