Browse Source

Merge branch 'main' into themes/fluent2

pull/21771/head
Max Katz 1 month ago
committed by GitHub
parent
commit
81ebe2b259
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      .github/copilot-instructions.md
  2. 2
      .gitignore
  3. 1
      .nuke/build.schema.json
  4. 2
      Avalonia.Desktop.slnf
  5. 11
      Avalonia.slnx
  6. 19
      CONTRIBUTING.md
  7. 2
      Directory.Build.props
  8. 20
      Directory.Packages.props
  9. 23
      SECURITY.md
  10. 220
      api/Avalonia.nupkg.xml
  11. 12
      azure-pipelines.yml
  12. 2
      build/SampleApp.props
  13. 2
      build/SharedVersion.props
  14. 1
      build/TargetFrameworks.props
  15. 8
      build/TrimmingEnable.props
  16. 6
      dirs.proj
  17. 2
      external/XamlX
  18. 4
      native/Avalonia.Native/src/OSX/AvnTextInputMethod.h
  19. 14
      native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm
  20. 1
      native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h
  21. 377
      native/Avalonia.Native/src/OSX/AvnView.mm
  22. 12
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  23. 46
      native/Avalonia.Native/src/OSX/PlatformSettings.mm
  24. 11
      native/Avalonia.Native/src/OSX/PopupImpl.mm
  25. 42
      native/Avalonia.Native/src/OSX/StorageProvider.mm
  26. 51
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  27. 2
      native/Avalonia.Native/src/OSX/WindowProtocol.h
  28. 101
      native/Avalonia.Native/src/OSX/automation.mm
  29. 15
      native/Avalonia.Native/src/OSX/menu.mm
  30. 33
      native/Avalonia.Native/src/OSX/metal.mm
  31. 6
      nukebuild/ApiDiffHelper.cs
  32. 40
      nukebuild/Build.cs
  33. 2
      nukebuild/BuildParameters.cs
  34. 740
      nukebuild/SbomGenerator.cs
  35. 4
      nukebuild/_build.csproj
  36. 17
      packages/Avalonia/AvaloniaBuildTasks.targets
  37. 2
      readme.md
  38. 2
      samples/AppWithoutLifetime/Program.cs
  39. 3
      samples/BindingDemo/App.xaml.cs
  40. 10
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  41. 1
      samples/ControlCatalog.Android/environment.device.txt
  42. 1
      samples/ControlCatalog.Android/environment.emulator.txt
  43. 3
      samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj
  44. 7
      samples/ControlCatalog.Desktop/NativeControls/Gtk/EmbedSample.Gtk.cs
  45. 54
      samples/ControlCatalog.Desktop/NativeControls/Mac/EmbedSample.Mac.cs
  46. 38
      samples/ControlCatalog.Desktop/NativeControls/Mac/MacHelper.cs
  47. 28
      samples/ControlCatalog.Desktop/NativeControls/Mac/ObjC.cs
  48. 4
      samples/ControlCatalog.Desktop/Program.cs
  49. 3
      samples/ControlCatalog/App.xaml
  50. 6
      samples/ControlCatalog/ControlCatalog.csproj
  51. 24
      samples/ControlCatalog/Converter/FlexDemoNumberToThicknessConverter.cs
  52. 31
      samples/ControlCatalog/Converter/GdpToBrushConverter.cs
  53. 27
      samples/ControlCatalog/Converter/TableViewColumnWidthConverter.cs
  54. 2
      samples/ControlCatalog/MainView.xaml
  55. 2
      samples/ControlCatalog/MainWindow.xaml
  56. 4
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
  57. 3
      samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
  58. 11
      samples/ControlCatalog/Pages/CalendarPage.xaml
  59. 3
      samples/ControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs
  60. 263
      samples/ControlCatalog/Pages/CompositionPage.axaml
  61. 179
      samples/ControlCatalog/Pages/CompositionPage.axaml.cs
  62. 12
      samples/ControlCatalog/Pages/DataGridPage.xaml
  63. 22
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  64. 4
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  65. 45
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  66. 219
      samples/ControlCatalog/Pages/FlexPage.axaml
  67. 37
      samples/ControlCatalog/Pages/FlexPage.axaml.cs
  68. 6
      samples/ControlCatalog/Pages/NotificationsPage.xaml
  69. 10
      samples/ControlCatalog/Pages/NotificationsPage.xaml.cs
  70. 2
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
  71. 136
      samples/ControlCatalog/Pages/PlatformSettingsPage.xaml
  72. 30
      samples/ControlCatalog/Pages/PlatformSettingsPage.xaml.cs
  73. 21
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  74. 7
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  75. 71
      samples/ControlCatalog/Pages/TableViewPage.axaml
  76. 13
      samples/ControlCatalog/Pages/TableViewPage.axaml.cs
  77. 23
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  78. 5
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  79. 70
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  80. 120
      samples/ControlCatalog/Pages/WrapPanelPage.axaml
  81. 13
      samples/ControlCatalog/Pages/WrapPanelPage.axaml.cs
  82. 2
      samples/ControlCatalog/ScrollPage.xaml
  83. 3
      samples/ControlCatalog/ViewModels/CursorPageViewModel.cs
  84. 2
      samples/ControlCatalog/ViewModels/DataValidationViewModel.cs
  85. 118
      samples/ControlCatalog/ViewModels/FlexItemViewModel.cs
  86. 126
      samples/ControlCatalog/ViewModels/FlexViewModel.cs
  87. 60
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  88. 4
      samples/ControlCatalog/ViewModels/MainWindowViewModel_PageList.cs
  89. 80
      samples/ControlCatalog/ViewModels/PlatformSettingsViewModel.cs
  90. 2
      samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
  91. 246
      samples/ControlCatalog/ViewModels/TableViewPageViewModel.cs
  92. 105
      samples/ControlCatalog/ViewModels/WrapPanelPageViewModel.cs
  93. 16
      samples/ControlGallery.WinUI/App.xaml
  94. 40
      samples/ControlGallery.WinUI/App.xaml.cs
  95. BIN
      samples/ControlGallery.WinUI/Assets/LockScreenLogo.scale-200.png
  96. BIN
      samples/ControlGallery.WinUI/Assets/SplashScreen.scale-200.png
  97. BIN
      samples/ControlGallery.WinUI/Assets/Square150x150Logo.scale-200.png
  98. BIN
      samples/ControlGallery.WinUI/Assets/Square44x44Logo.scale-200.png
  99. BIN
      samples/ControlGallery.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
  100. BIN
      samples/ControlGallery.WinUI/Assets/StoreLogo.png

13
.github/copilot-instructions.md

@ -1,3 +1,16 @@
When running code review, please refer to the following documents if they are relevant to the changed parts of the codebase:
- `native/Avalonia.Native/**` -> `native/Avalonia.Native/README.md`
- `src/Avalonia.Wayland/**` -> `src/Avalonia.Wayland/README.md`
## SBOM (EU Cyber Resilience Act)
We generate a CycloneDX SBOM per published NuGet package (`nukebuild/SbomGenerator.cs`, `CreateSbom` target) so that every shipped package carries an accurate bill of materials, as required for EU CRA compliance. When reviewing, flag the following:
- **New shipped package:** if a change adds a new NuGet package that is published to end users (a new packable project, or a new final package in `nukebuild/numerge.json`), it must be covered by SBOM generation. Confirm `CreateSbom` produces an SBOM for it, and call it out if the package would ship without one.
- **New kinds of shipped dependencies:** if a change alters what dependencies are delivered with a component in a way the existing scan doesn't already understand, `nukebuild/SbomGenerator.cs` must be updated so those dependencies appear in the SBOM. In particular:
- Adding npm/JS dependencies to a component that was previously .NET-only (e.g. a new `webapp` bundled into a package), or changing how existing bundled JS is built.
- Bundling third-party binaries directly into a package (copy-local/ILRepack/embedded assemblies) rather than referencing them as normal NuGet dependencies.
- New Numerge merge groups, or any other mechanism that folds a project's shipped output into a package other than via ordinary `PackageReference`/`ProjectReference`.
The scope is what is *delivered* with the package (BSI TR-03183-2 "scope of delivery"), not build/test-only tooling. Purely development-time dependencies that are never shipped do not need to be in the SBOM.

2
.gitignore

@ -227,3 +227,5 @@ src/Browser/Avalonia.Browser/staticwebassets
/.claude/settings.local.json
/planning
BenchmarkDotNet.Artifacts.*
.tokensave
/CLAUDE.md

1
.nuke/build.schema.json

@ -33,6 +33,7 @@
"CompileNative",
"CreateIntermediateNugetPackages",
"CreateNugetPackages",
"CreateSbom",
"DownloadApiBaselinePackages",
"GenerateCppHeaders",
"GenerateUnicodeData",

2
Avalonia.Desktop.slnf

@ -34,6 +34,7 @@
"src\\Avalonia.Themes.Fluent\\Avalonia.Themes.Fluent.csproj",
"src\\Avalonia.Themes.Simple\\Avalonia.Themes.Simple.csproj",
"src\\Avalonia.Vulkan\\Avalonia.Vulkan.csproj",
"src\\Avalonia.Wayland\\Avalonia.Wayland.csproj",
"src\\Avalonia.X11\\Avalonia.X11.csproj",
"src\\HarfBuzz\\Avalonia.HarfBuzz\\Avalonia.HarfBuzz.csproj",
"src\\Headless\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj",
@ -72,6 +73,7 @@
"tests\\Avalonia.Skia.RenderTests\\Avalonia.Skia.RenderTests.csproj",
"tests\\Avalonia.Skia.UnitTests\\Avalonia.Skia.UnitTests.csproj",
"tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj",
"tests\\Avalonia.Wayland.UnitTests\\Avalonia.Wayland.UnitTests.csproj",
"tests\\TestFiles\\BuildTasks\\PInvoke\\PInvoke.csproj"
]
}

11
Avalonia.slnx

@ -22,6 +22,7 @@
<Project Path="src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj" />
<Project Path="src/Avalonia.X11/Avalonia.X11.csproj" />
<Project Path="src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj" />
<Project Path="src/Avalonia.Wayland/Avalonia.Wayland.csproj" />
</Folder>
<Folder Name="/Markup/">
<Project Path="src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj" />
@ -65,6 +66,10 @@
<Project Path="samples/ControlCatalog.MacCatalyst/ControlCatalog.MacCatalyst.csproj" />
<Project Path="samples/ControlCatalog.tvOS/ControlCatalog.tvOS.csproj" />
<Project Path="samples/ControlCatalog/ControlCatalog.csproj" />
<Project Path="samples/ControlGallery.WinUI/ControlGallery.WinUI.csproj">
<Platform Project="x64" />
<Deploy />
</Project>
<Project Path="samples/Generators.Sandbox/Generators.Sandbox.csproj" />
<Project Path="samples/GpuInterop/GpuInterop.csproj" />
<Project Path="samples/IntegrationTestApp/IntegrationTestApp.csproj" />
@ -84,6 +89,10 @@
<Project Path="samples/SingleProjectSandbox/SingleProjectSandbox.csproj" />
<Project Path="samples/TextTestApp/TextTestApp.csproj" />
<Project Path="samples/VirtualizationDemo/VirtualizationDemo.csproj" />
<Project Path="samples/WinUIEmbedSample/WinUIEmbedSample.csproj">
<Platform Project="x64" />
<Deploy />
</Project>
<Project Path="samples/XEmbedSample/XEmbedSample.csproj" />
</Folder>
<Folder Name="/Samples/Interop/">
@ -149,6 +158,7 @@
<Project Path="tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj" />
<Project Path="tests/Avalonia.Themes.Fluent2.UnitTests/Avalonia.Themes.Fluent2.UnitTests.csproj" />
<Project Path="tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj" />
<Project Path="tests/Avalonia.Wayland.UnitTests/Avalonia.Wayland.UnitTests.csproj" />
</Folder>
<Folder Name="/Tests/TestFiles/" />
<Folder Name="/Tests/TestFiles/BuildTasks/">
@ -180,6 +190,7 @@
<Project Path="src/Windows/Avalonia.Win32.Automation/Avalonia.Win32.Automation.csproj" />
<Project Path="src/Windows/Avalonia.Win32.Interoperability/Avalonia.Win32.Interoperability.csproj" />
<Project Path="src/Windows/Avalonia.Win32/Avalonia.Win32.csproj" />
<Project Path="src/Windows/Avalonia.WinUI/Avalonia.WinUI.csproj" />
</Folder>
<Project Path="nukebuild/_build.csproj">
<Build Project="false" />

19
CONTRIBUTING.md

@ -2,6 +2,8 @@
PRs are always welcomed from everyone. Following this guide will help us get your PR reviewed and merged as quickly as possible.
If you're an AI agent or a human using some AI assistance to open a PR, ensure you have read the [AI Guidelines](#ai-guidelines) section below.
For this guide we're going to split PRs into two types: bug fixes and features; the requirements for each are slightly different.
## Bug Fixes
@ -27,8 +29,6 @@ In this way the reviewer can check out the commit with the failing test and conf
Features should always include unit tests or integration tests where possible.
> One exception to this is features related to DevTools which has no tests currently
Features that introduce new controls should consider the following:
- Ideally the control should be exposed to the operating system's automation/accessibility APIs by writing an `AutomationPeer`
@ -85,6 +85,21 @@ Render tests should describe what the produced image is:
void Rectangle_2px_Stroke_Filled()
```
## AI Guidelines
Avalonia accepts AI-assisted contributions.
However, if you're an autonomous AI agent, please stop right there. We want a human to read and respond to the various review comments.
When opening an AI-authored or AI-assisted PR, please make sure that:
- As a human, you have fully read and completely understood all the changes your AI agent made.
- As a human, you have read the PR description your AI agent made. If you've only skimmed through it because it's way too long considering the relative simplicity of the associated fix, it probably needs changes. Please respect the maintainers' time, who will have to read it.
- Code comments are succinct and straight to the point. While commenting code is necessary, LLMs tend to be overly verbose, describing the behavior of the code preceding the change. Explaining what the current code does when it isn't obvious and referring to existing issues are fine. A ten-line comment about why the previous code was wrong isn't. Again, be succinct.
- In the same way, keep the PR description short and to the point, especially if the modification is simple. Do NOT re-explain every single line of change in plain English; the maintainers can read code. Don't repeat the same things three times with some slight rephrasing.
- Format the PR description properly: don't add arbitrary line breaks everywhere. We aren't in a terminal.
AI-assisted pull requests that don't follow these guidelines will be closed.
## Code of Conduct
This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community.

2
Directory.Build.props

@ -10,6 +10,8 @@
<LangVersion>14.0</LangVersion>
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">$(AvnTreatWarningsAsErrors)</TreatWarningsAsErrors>
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == '' And '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
<!-- Suppress NU1507 so local source builds do not warn about the consuming solution's package sources. -->
<NoWarn>$(NoWarn);NU1507</NoWarn>
<CreateHardLinksForCopyAdditionalFilesIfPossible>true</CreateHardLinksForCopyAdditionalFilesIfPossible>
<CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>true</CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>
<CreateHardLinksForCopyLocalIfPossible>true</CreateHardLinksForCopyLocalIfPossible>

20
Directory.Packages.props

@ -2,12 +2,11 @@
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Appium.WebDriver" Version="5.2.0" />
<PackageVersion Include="Avalonia.Angle.Windows.Natives" Version="2.1.27548.20260419" />
<PackageVersion Include="Avalonia.BuildServices" Version="11.3.2" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.1" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.2" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.6" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="Dotnet.Bundle" Version="0.9.13" />
@ -23,23 +22,27 @@
<PackageVersion Include="Microsoft.Build.Framework" Version="18.0.2" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="15.1.548" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic" Version="4.5.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.5.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.14.0" />
<PackageVersion Include="Microsoft.CSharp" Version="4.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="Microsoft.Reactive.Testing" Version="6.1.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="2.0.2" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.257" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7705" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.260209005" />
<PackageVersion Include="Mono.Cecil" Version="0.11.6" />
<PackageVersion Include="MonoMac.NetStandard" Version="0.0.4" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Nito.AsyncEx.Context" Version="5.1.2" />
<PackageVersion Include="NuGet.Protocol" Version="7.0.1" />
<PackageVersion Include="NuGet.Protocol" Version="7.9.0" />
<PackageVersion Include="Nuke.Common" Version="10.1.0" />
<PackageVersion Include="Numerge" Version="1.0.0" />
<PackageVersion Include="NUnit" Version="4.5.1" />
<PackageVersion Include="NUnit3TestAdapter" Version="6.2.0" />
<PackageVersion Include="NWayland" Version="0.11.0" />
<PackageVersion Include="NWayland.Server" Version="0.11.0" />
<PackageVersion Include="Quamotion.RemoteViewing" Version="1.1.211" />
<PackageVersion Include="SharpCompress" Version="0.48.0" />
<PackageVersion Include="Silk.NET.Direct3D.Compilers" Version="2.22.0" />
@ -62,17 +65,18 @@
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.94.1" />
<PackageVersion Include="unofficial.mesa.softwarerenderer" Version="25.3.6.29" />
<PackageVersion Include="Tmds.DBus.Generator" Version="0.94.1" />
<PackageVersion Include="Xamarin.AndroidX.AppCompat" Version="1.7.1.3" />
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.2.0.2" />
<PackageVersion Include="Xamarin.AndroidX.DocumentFile" Version="1.1.0.3" />
<PackageVersion Include="Xamarin.AndroidX.Window" Version="1.5.1.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="Xunit.StaFact" Version="3.0.13" />
<PackageVersion Include="xunit.v3.extensibility.core" Version="3.2.2" />
<PackageVersion Include="xunit.v3.mtp-v2" Version="3.2.2" />
</ItemGroup>
<ItemGroup>
<GlobalPackageReference Include="Microsoft.VisualStudio.SlnGen" Version="8.5.17" PrivateAssets="all" />
</ItemGroup>
</Project>
</Project>

23
SECURITY.md

@ -1,4 +1,5 @@
# Security Policy
Avalonia is free and open-source software published by AvaloniaUI OÜ. The framework is made publicly available at no charge and is not placed on the market within the meaning of the EU Cyber Resilience Act (Regulation (EU) 2024/2847). AvaloniaUI OÜ acts as the project's open-source software steward and maintains this cybersecurity policy in that capacity, in line with Article 24 of that Regulation.
## Reporting a Vulnerability
@ -21,5 +22,25 @@ https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/
### Reporting via Email
Alternatively, you may report security vulnerabilities by emailing security@avaloniaui.net.
### Misc
### What to Expect
- We aim to acknowledge your report within **2 business days**.
- We aim to confirm whether we consider it a vulnerability and to share a remediation timeline within **10 business days**.
- We will keep you informed of progress and coordinate the disclosure date with you.
- We will credit you in the published advisory unless you ask us not to.
These timescales are the targets we work to for the open-source project; they are not contractual commitments. Organisations that require contractually binding response and remediation times can obtain them under a commercial agreement.
Please note that Avalonia does not operate a bug bounty programme.
## Versions Receiving Security Fixes
| Version | Security fixes |
|---|---|
| 11.x and older | Not provided |
| 12.x (current stable) | Provided |
Security fixes are delivered at the head of the current stable series. If a vulnerability affects earlier 12.x releases, we fix it in a new release of the latest version, and the remediation path is to upgrade to that release. Security fixes for the current series are published openly and free of charge. Access to Avalonia's open-source releases, updates and security fixes is never conditional on payment.
## Scope
This policy covers the Avalonia framework packages published from this repository (`Avalonia` and the `Avalonia.*` platform and integration packages).

220
api/Avalonia.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
@ -19,12 +19,60 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,System.Nullable{System.Int32})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IBitmapImpl.Save(System.String,System.Nullable{System.Int32})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.#ctor(Avalonia.PixelSize,System.Double,Avalonia.Size)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.#ctor(Avalonia.PixelSize,System.Double)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.Deconstruct(Avalonia.PixelSize@,System.Double@,Avalonia.Size@)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.Deconstruct(Avalonia.PixelSize@,System.Double@)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetFrameThemeVariant(Avalonia.Platform.PlatformThemeVariant)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.ITopLevelImpl.SetFrameThemeVariant(Avalonia.Platform.PlatformThemeVariant)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDraw(Avalonia.OpenGL.Egl.EglSurface,Avalonia.PixelSize,System.Double,System.Action,System.Boolean)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.OpenGL.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.OpenGL.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.PointerEventArgs.#ctor(Avalonia.Interactivity.RoutedEvent,System.Object,Avalonia.Input.IPointer,Avalonia.Visual,Avalonia.Point,System.UInt64,Avalonia.Input.PointerPointProperties,Avalonia.Input.KeyModifiers,System.Lazy{System.Collections.Generic.IReadOnlyList{Avalonia.Input.Raw.RawPointerPoint}})</Target>
@ -43,10 +91,178 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,System.Nullable{System.Int32})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IBitmapImpl.Save(System.String,System.Nullable{System.Int32})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.#ctor(Avalonia.PixelSize,System.Double,Avalonia.Size)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.#ctor(Avalonia.PixelSize,System.Double)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.Deconstruct(Avalonia.PixelSize@,System.Double@,Avalonia.Size@)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.Deconstruct(Avalonia.PixelSize@,System.Double@)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
</Suppressions>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetFrameThemeVariant(Avalonia.Platform.PlatformThemeVariant)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.ITopLevelImpl.SetFrameThemeVariant(Avalonia.Platform.PlatformThemeVariant)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.OpenGL.Egl.EglPlatformSurfaceRenderTargetBase.BeginDraw(Avalonia.OpenGL.Egl.EglSurface,Avalonia.PixelSize,System.Double,System.Action,System.Boolean)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>E:Avalonia.Platform.IPlatformSettings.PreferredApplicationLanguageChanged</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,Avalonia.Media.Imaging.BitmapEncoderOptions)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IGeometryImpl.FillContains(Avalonia.Platform.IGeometryImpl)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IGeometryImpl.GetFillIntersectionResult(Avalonia.Platform.IGeometryImpl)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.Storage.IStorageProvider.OpenFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerOpenOptions)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IImageBrushSource.Bitmap</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Platform.IPlatformSettings.PreferredApplicationLanguage</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl.SetWindowCornerPreference(Avalonia.Controls.Win32Properties.WindowCornerPreference)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPopupImpl.SetHitTestVisible(System.Boolean)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.ITopLevelImpl.SetFrameThemeVariant(System.Nullable{Avalonia.Platform.PlatformThemeVariant})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>E:Avalonia.Platform.IPlatformSettings.PreferredApplicationLanguageChanged</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,Avalonia.Media.Imaging.BitmapEncoderOptions)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IGeometryImpl.FillContains(Avalonia.Platform.IGeometryImpl)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IGeometryImpl.GetFillIntersectionResult(Avalonia.Platform.IGeometryImpl)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.Storage.IStorageProvider.OpenFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerOpenOptions)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IImageBrushSource.Bitmap</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Platform.IPlatformSettings.PreferredApplicationLanguage</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl.SetWindowCornerPreference(Avalonia.Controls.Win32Properties.WindowCornerPreference)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IPopupImpl.SetHitTestVisible(System.Boolean)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.ITopLevelImpl.SetFrameThemeVariant(System.Nullable{Avalonia.Platform.PlatformThemeVariant})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
</Suppressions>

12
azure-pipelines.yml

@ -132,6 +132,12 @@ jobs:
artifactName: 'NuGetOSX'
condition: succeeded()
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.SourcesDirectory)/artifacts/sbom'
artifactName: 'SBOMOSX'
condition: succeeded()
- job: Windows
pool:
vmImage: 'windows-2025'
@ -187,3 +193,9 @@ jobs:
artifactName: 'Samples'
condition: succeeded()
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.SourcesDirectory)/artifacts/sbom'
artifactName: 'SBOM'
condition: succeeded()

2
build/SampleApp.props

@ -14,7 +14,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" />
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Condition="'$(Configuration)' == 'Debug'" />
</ItemGroup>
<Target Name="GatherReferences" AfterTargets="CoreCompile">

2
build/SharedVersion.props

@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Product>Avalonia</Product>
<Version>12.1.999</Version>
<Version>12.2.999</Version>
<Authors>Avalonia Team</Authors>
<Copyright>Copyright 2013-$([System.DateTime]::Now.ToString(`yyyy`)) &#169; The AvaloniaUI Project</Copyright>
<PackageProjectUrl>https://avaloniaui.net/?utm_source=nuget&amp;utm_medium=referral&amp;utm_content=project_homepage_link</PackageProjectUrl>

1
build/TargetFrameworks.props

@ -8,6 +8,7 @@
<AvsCurrentIOSTargetFramework>$(AvsCurrentTargetFramework)-ios26.0</AvsCurrentIOSTargetFramework>
<AvsCurrentTvOSTargetFramework>$(AvsCurrentTargetFramework)-tvos26.0</AvsCurrentTvOSTargetFramework>
<AvsCurrentBrowserTargetFramework>$(AvsCurrentTargetFramework)-browser</AvsCurrentBrowserTargetFramework>
<AvsCurrentWinUITargetFramework>$(AvsCurrentTargetFramework)-windows10.0.19041.0</AvsCurrentWinUITargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(AvsSkipBuildingLegacyTargetFrameworks)' != 'True'">
<AvsLegacyTargetFrameworks>net8.0</AvsLegacyTargetFrameworks>

8
build/TrimmingEnable.props

@ -1,10 +1,10 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<IsTrimmable>true</IsTrimmable>
<SuppressTrimAnalysisWarnings Condition="'$(SuppressTrimAnalysisWarnings)' == ''">false</SuppressTrimAnalysisWarnings>
<EnableTrimAnalyzer Condition="'$(EnableTrimAnalyzer)' == ''">true</EnableTrimAnalyzer>
<TrimmerSingleWarn Condition="'$(TrimmerSingleWarn)' == ''">false</TrimmerSingleWarn>
<IsTrimmable Condition="'$(IsTrimmable)' == ''">true</IsTrimmable>
<IsAotCompatible Condition="'$(IsAotCompatible)' == ''">true</IsAotCompatible>
<ILLinkTreatWarningsAsErrors>$(TreatWarningsAsErrors)</ILLinkTreatWarningsAsErrors>
</PropertyGroup>

6
dirs.proj

@ -14,12 +14,18 @@
<ProjectReference Remove="samples/*.Android/*.csproj" />
<ProjectReference Remove="samples/*.Browser/*.csproj" />
<ProjectReference Remove="samples/SingleProjectSandbox/*.csproj" />
<!-- Exclude WinUI samples when Platform is not specified or is AnyCPU -->
<ProjectReference Remove="samples/*.WinUI/*.csproj" Condition="'$(Platform)' == '' Or '$(Platform)' == 'AnyCPU'" />
<ProjectReference Remove="samples/WinUIEmbedSample/*.csproj" Condition="'$(Platform)' == '' Or '$(Platform)' == 'AnyCPU'" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsOsPlatform('Windows'))">
<!-- Build mobile backends only on Windows, where we have installed android workload -->
<ProjectReference Remove="src/Android/**/*.*proj" />
<ProjectReference Remove="src/iOS/**/*.*proj" />
<ProjectReference Remove="src/Windows/Avalonia.WinUI/**/*.*proj" />
<ProjectReference Remove="samples/*.WinUI/*.csproj" />
<ProjectReference Remove="samples/WinUIEmbedSample/*.csproj" />
</ItemGroup>
</Project>

2
external/XamlX

@ -1 +1 @@
Subproject commit 86f5a26fadb745e95169c3c805ebd9fd7c9b44d2
Subproject commit d7e37ca63dc9b13cdc95ca165938d4904fa0eddf

4
native/Avalonia.Native/src/OSX/AvnTextInputMethod.h

@ -38,7 +38,9 @@ public:
virtual void SetCursorRect (AvnRect rect) override;
virtual void SetSurroundingText (char* text, int anchorOffset, int cursorOffset) override;
virtual void SetSurroundingText (char* text, int start, int end) override;
virtual void SetSelectionInSurroundingText (int start, int end) override;
public:
ComPtr<IAvnTextInputMethodClient> Client;

14
native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm

@ -29,13 +29,21 @@ HRESULT AvnTextInputMethod::SetClient(IAvnTextInputMethodClient *client) {
}
void AvnTextInputMethod::Reset() {
[_inputMethodDelegate resetInputMethod];
}
void AvnTextInputMethod::SetSurroundingText(char* text, int anchorOffset, int cursorOffset) {
[_inputMethodDelegate setText:[NSString stringWithUTF8String:text]];
[_inputMethodDelegate setSelection: anchorOffset : cursorOffset];
void AvnTextInputMethod::SetSurroundingText(char* text, int start, int end) {
// stringWithUTF8String: throws on a null pointer and returns nil for invalid UTF-8.
NSString* surroundingText = text != nullptr ? [NSString stringWithUTF8String:text] : nil;
[_inputMethodDelegate setText:surroundingText != nil ? surroundingText : @""];
[_inputMethodDelegate setSelection: start:end];
}
void AvnTextInputMethod::SetCursorRect(AvnRect rect) {
[_inputMethodDelegate setCursorRect: rect];
}
void AvnTextInputMethod::SetSelectionInSurroundingText(int start, int end) {
[_inputMethodDelegate setSelection: start:end];
}

1
native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h

@ -14,6 +14,7 @@
-(void) setText:(NSString* _Nonnull) text;
-(void) setCursorRect:(AvnRect) cursorRect;
-(void) setSelection: (int) start : (int) end;
-(void) resetInputMethod;
@end

377
native/Avalonia.Native/src/OSX/AvnView.mm

@ -4,6 +4,7 @@
//
#import <AppKit/AppKit.h>
#import <Carbon/Carbon.h> /* For the TIS functions used to classify the keyboard input source. */
#include "AvnView.h"
#include "automation.h"
#import "WindowInterfaces.h"
@ -22,8 +23,9 @@
NSMutableAttributedString* _text;
NSRange _selectedRange;
NSRange _markedRange;
NSEvent* _lastKeyDownEvent;
NSMutableArray* _accessibilityChildren;
NSString* _keyboardInputSourceId;
bool _keyboardInputSourceComposes;
}
- (void)onClosed
@ -375,7 +377,15 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
[self becomeFirstResponder];
}
}
if([self hasMarkedText] &&
(type == LeftButtonDown || type == RightButtonDown || type == MiddleButtonDown ||
type == XButton1Down || type == XButton2Down) &&
[self inputContext] != nil)
{
[[self inputContext] handleEvent:event];
}
auto parent = _parent.tryGet();
if(parent != nullptr)
{
@ -641,9 +651,100 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
[super flagsChanged:event];
}
- (bool) handleKeyDown: (NSTimeInterval) timestamp withKey:(AvnKey)key withPhysicalKey:(AvnPhysicalKey)physicalKey withModifiers:(AvnInputModifiers)modifiers withKeySymbol:(NSString*)keySymbol {
auto parent = _parent.tryGet();
return parent->TopLevelEvents->RawKeyEvent(KeyDown, timestamp, modifiers, key, physicalKey, [keySymbol UTF8String]);
// The input mode an input method uses for direct alphanumeric typing. In this mode the input
// method passes keys straight through instead of composing, so keys must not be masked.
static CFStringRef const AlphanumericInputModeId = CFSTR("com.apple.inputmethod.Roman");
static bool InputSourceComposes(TISInputSourceRef source)
{
// A plain keyboard layout never composes. Dead keys are not driven by the input source, they
// show up as a composition in progress and are covered by hasMarkedText.
auto type = static_cast<CFStringRef>(TISGetInputSourceProperty(source, kTISPropertyInputSourceType));
if(type == nullptr || CFEqual(type, kTISTypeKeyboardLayout))
{
return false;
}
auto mode = static_cast<CFStringRef>(TISGetInputSourceProperty(source, kTISPropertyInputModeID));
return mode == nullptr || !CFEqual(mode, AlphanumericInputModeId);
}
// Whether this keystroke could be the one that starts a composition. Only a key that produces a
// printable character can, and masking any of the others would stop controls like TextBox from
// reacting to them at all.
static bool CanStartComposition(NSString* keySymbol, NSEventModifierFlags modifierFlags)
{
// No symbol at all: the arrows, the function keys, Home, End and friends.
if(keySymbol == nullptr || [keySymbol length] == 0)
{
return false;
}
// KeySymbolFromScanCode deliberately reports the control character for Backspace, Enter, Tab
// and Escape, and Forward Delete reports DEL (0x7F), so a symbol alone does not mean the key
// produces text. Those keys edit or cancel, they never start a composition.
auto firstChar = [keySymbol characterAtIndex:0];
if(firstChar < 0x20 || firstChar == 0x7F)
{
return false;
}
// Command and Control combinations are shortcuts, which an input method does not consume.
return (modifierFlags & (NSEventModifierFlagCommand | NSEventModifierFlagControl)) == 0;
}
// Returns true when the input source with the given id is an input method that can start a
// composition, as opposed to a plain keyboard layout or an input method in alphanumeric mode.
static bool KeyboardInputSourceComposes(NSString* sourceId)
{
NSDictionary* filter = @{ (__bridge NSString*)kTISPropertyInputSourceID: sourceId };
// Include every installed source, not just the enabled ones: an input method mode can be
// selected without being listed as enabled, and failing to find it would classify a
// composing input method as a plain layout.
auto sources = TISCreateInputSourceList((__bridge CFDictionaryRef)filter, true);
if(sources == nullptr)
{
return false;
}
auto composes = CFArrayGetCount(sources) > 0 &&
InputSourceComposes((TISInputSourceRef)CFArrayGetValueAtIndex(sources, 0));
CFRelease(sources);
return composes;
}
// Whether the currently selected keyboard input source is a composing input method. The result
// is cached per input source id, since this is queried for every key down.
- (bool)isComposingInputSourceSelected
{
auto inputContext = [self inputContext];
if(inputContext == nullptr)
{
return false;
}
NSString* sourceId = [inputContext selectedKeyboardInputSource];
if(sourceId == nullptr)
{
return false;
}
if(![sourceId isEqualToString:_keyboardInputSourceId])
{
_keyboardInputSourceId = sourceId;
_keyboardInputSourceComposes = KeyboardInputSourceComposes(sourceId);
}
return _keyboardInputSourceComposes;
}
- (void)keyDown:(NSEvent *)event
@ -653,57 +754,57 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
{
return;
}
_lastKeyDownEvent = event;
auto timestamp = static_cast<uint64_t>([event timestamp] * 1000);
auto scanCode = [event keyCode];
auto key = VirtualKeyFromScanCode(scanCode, [event modifierFlags]);
auto physicalKey = PhysicalKeyFromScanCode(scanCode);
auto keySymbol = KeySymbolFromScanCode(scanCode, [event modifierFlags]);
auto keySymbolUtf8 = keySymbol == nullptr ? nullptr : [keySymbol UTF8String];
auto modifiers = [self getModifiers:[event modifierFlags]];
//InputMethod is active
if(parent->InputMethod->IsActive()){
auto hasInputModifier = modifiers != AvnInputModifiersNone;
//Handle keyDown first if an input modifier is present
if(hasInputModifier){
if([self handleKeyDown:timestamp withKey:key withPhysicalKey:physicalKey withModifiers:modifiers withKeySymbol:keySymbol]){
//User code has handled the event
_lastKeyDownEvent = nullptr;
return;
}
}
if([[self inputContext] handleEvent:event] == NO){
//KeyDown has not been consumed by the input context
//Only raise a keyDown if we don't have a modifier
if(!hasInputModifier){
[self handleKeyDown:timestamp withKey:key withPhysicalKey:physicalKey withModifiers:modifiers withKeySymbol:keySymbol];
}
}
// The keystroke belongs to the input method when a composition is already in progress, since
// it then selects a candidate, commits or cancels it. It also belongs to the input method
// when a character key is typed into a text input client while a composing input method is
// selected, because that keystroke may be the one that starts the composition and we cannot
// know that before the input context has seen it.
//
// In both cases mask the key the way Win32 reports VK_PROCESSKEY, so that user code still
// observes a KeyDown but no KeyGesture can match it. The physical key and the key symbol
// keep their real values, so the underlying key remains recoverable.
//
// Both cases require an active client: without one the input context is never consulted, so
// marked text left over from a composition that lost its client mid-way must not mask keys.
auto imeProcessed = parent->InputMethod->IsActive() &&
([self hasMarkedText] ||
(CanStartComposition(keySymbol, [event modifierFlags]) &&
[self isComposingInputSourceSelected]));
auto key = imeProcessed
? AvnKeyImeProcessed
: VirtualKeyFromScanCode(scanCode, [event modifierFlags]);
// A KeyDown is always raised before the input context sees the event, otherwise the
// input context silently consumes printable keys (space, letters, digits) and user code
// never gets a chance to react to them.
auto handled = parent->TopLevelEvents->RawKeyEvent(KeyDown, timestamp, modifiers, key, physicalKey, keySymbolUtf8);
if(handled)
{
// User code has handled the event, so no text may be produced from it.
return;
}
//InputMethod not active
else{
auto keyDownHandled = [self handleKeyDown:timestamp withKey:key withPhysicalKey:physicalKey withModifiers:modifiers withKeySymbol:keySymbol];
//Raise text input event for unhandled key down
if(!keyDownHandled){
if(keySymbol != nullptr && key != AvnKeyEnter){
auto timestamp = static_cast<uint64_t>([event timestamp] * 1000);
parent->TopLevelEvents->RawTextInputEvent(timestamp, [keySymbol UTF8String]);
}
}
if(parent->InputMethod->IsActive())
{
// Let the input context produce the text or drive the composition.
[[self inputContext] handleEvent:event];
}
else if(keySymbol != nullptr && key != AvnKeyEnter)
{
parent->TopLevelEvents->RawTextInputEvent(timestamp, keySymbolUtf8);
}
_lastKeyDownEvent = nullptr;
}
- (void)keyUp:(NSEvent *)event
@ -713,9 +814,9 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
}
- (void) doCommandBySelector:(SEL)selector{
if(_lastKeyDownEvent != nullptr){
[self keyboardEvent:_lastKeyDownEvent withType:KeyDown];
}
// -keyDown: already raised a KeyDown for this event before handing it to the input
// context, so nothing is left to do here. The method still has to be implemented:
// falling back to NSResponder would perform the default action and emit a system beep.
}
- (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod
@ -747,25 +848,48 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
return (AvnInputModifiers)rv;
}
// Clamps a range so that it can never be used to index outside of _text.
// Ranges reaching us from AppKit or from the managed side are not guaranteed to be valid.
- (NSRange)clampRangeToText:(NSRange)range
{
if (range.location == NSNotFound)
return NSMakeRange(NSNotFound, 0);
NSUInteger length = _text.length;
if (range.location > length)
return NSMakeRange(length, 0);
// Avoids the overflow of location + length that a plain bounds check would have.
return NSMakeRange(range.location, MIN(range.length, length - range.location));
}
- (BOOL)hasMarkedText
{
return _markedRange.length > 0;
return _markedRange.location != NSNotFound && _markedRange.length > 0;
}
- (NSRange)markedRange
{
return _markedRange;
// From the docs: returns {NSNotFound, 0} if there is no marked range.
if (![self hasMarkedText])
return NSMakeRange(NSNotFound, 0);
// The preedit isn't necessarily part of the surrounding text we got from the managed side,
// so only the location is clamped here. An overlong length is handled by
// attributedSubstringForProposedRange:actualRange:, as the docs require.
return NSMakeRange(MIN(_markedRange.location, _text.length), _markedRange.length);
}
- (NSRange)selectedRange
{
return _selectedRange;
return [self clampRangeToText:_selectedRange];
}
- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange
{
NSString* markedText;
if([string isKindOfClass:[NSAttributedString class]])
{
markedText = [string string];
@ -774,24 +898,47 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
{
markedText = (NSString*) string;
}
_markedRange = NSMakeRange(_selectedRange.location, [markedText length]);
if (markedText == nil)
{
markedText = @"";
}
auto parent = _parent.tryGet();
if(parent->InputMethod->IsActive()){
parent->InputMethod->Client->SetPreeditText((char*)[markedText UTF8String]);
// Delete any replaced range
auto finalReplacementRange = [self clampRangeToText:replacementRange];
if (finalReplacementRange.location != NSNotFound && parent != nullptr && parent->InputMethod->IsActive())
{
parent->InputMethod->Client->SelectInSurroundingText((int)finalReplacementRange.location, (int)(finalReplacementRange.location + finalReplacementRange.length));
uint64_t timestamp = static_cast<uint64_t>([NSDate timeIntervalSinceReferenceDate] * 1000);
parent->TopLevelEvents->RawKeyEvent(KeyDown, timestamp, AvnInputModifiersNone, AvnKeyBack, AvnPhysicalKeyNone, "\b");
parent->TopLevelEvents->RawKeyEvent(KeyUp, timestamp, AvnInputModifiersNone, AvnKeyBack, AvnPhysicalKeyNone, "\b");
}
auto markedLocation = [self selectedRange].location;
_markedRange = NSMakeRange(markedLocation == NSNotFound ? 0 : markedLocation, [markedText length]);
if (parent != nullptr && parent->InputMethod->IsActive())
{
const char* utf8MarkedText = [markedText UTF8String];
parent->InputMethod->Client->SetPreeditText((char*)(utf8MarkedText != nullptr ? utf8MarkedText : ""));
}
}
- (void)unmarkText
{
auto parent = _parent.tryGet();
if(parent->InputMethod->IsActive()){
if(parent != nullptr && parent->InputMethod->IsActive()){
parent->InputMethod->Client->SetPreeditText(nullptr);
}
_markedRange = NSMakeRange(_selectedRange.location, 0);
auto selectionLocation = [self selectedRange].location;
_markedRange = NSMakeRange(selectionLocation == NSNotFound ? 0 : selectionLocation, 0);
if([self inputContext]) {
[[self inputContext] discardMarkedText];
}
@ -804,13 +951,27 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange
{
if(actualRange){
range = *actualRange;
// From the docs: an implementation of this method should be prepared for aRange to be out of bounds.
// In this case, you should return the intersection of the document's range and aRange.
// If the location of aRange is completely outside of the document's range, return nil.
// actualRange is an out parameter: it is uninitialized on entry and must only be written to.
NSRange docRange = NSMakeRange(0, _text.length);
NSRange finalRange = NSIntersectionRange(range, docRange);
if (finalRange.length == 0)
{
if (actualRange) {
*actualRange = NSMakeRange(NSNotFound, 0);
}
return nil;
}
NSAttributedString* subString = [_text attributedSubstringFromRange:range];
return subString;
if (actualRange) {
*actualRange = finalRange;
}
return [_text attributedSubstringFromRange:finalRange];
}
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
@ -819,9 +980,9 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
if(parent == nullptr){
return;
}
NSString* text;
if([string isKindOfClass:[NSAttributedString class]])
{
text = [string string];
@ -830,12 +991,28 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
{
text = (NSString*) string;
}
if (text == nil)
{
text = @"";
}
auto finalReplacementRange = [self clampRangeToText:replacementRange];
if (finalReplacementRange.location != NSNotFound &&
![self hasMarkedText] &&
parent->InputMethod->IsActive())
{
parent->InputMethod->Client->SelectInSurroundingText((int)finalReplacementRange.location, (int)(finalReplacementRange.location + finalReplacementRange.length));
}
[self unmarkText];
uint64_t timestamp = static_cast<uint64_t>([NSDate timeIntervalSinceReferenceDate] * 1000);
parent->TopLevelEvents->RawTextInputEvent(timestamp, [text UTF8String]);
const char* utf8Text = [text UTF8String];
parent->TopLevelEvents->RawTextInputEvent(timestamp, utf8Text != nullptr ? utf8Text : "");
}
- (NSUInteger)characterIndexForPoint:(NSPoint)point
@ -845,11 +1022,17 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange
{
// actualRange is an out parameter: it is uninitialized on entry and must only be written to.
// We only ever report a single rect, so the requested range is echoed back clamped to the document.
if (actualRange) {
*actualRange = [self clampRangeToText:range];
}
auto parent = _parent.tryGet();
if(!parent->InputMethod->IsActive()){
if(parent == nullptr || !parent->InputMethod->IsActive()){
return NSZeroRect;
}
return _cursorRect;
}
@ -1003,11 +1186,45 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
}
- (void) setText:(NSString *)text{
[[_text mutableString] setString:text];
[[_text mutableString] setString:text != nil ? text : @""];
// The document changed, so the stored ranges can now point outside of it.
_selectedRange = [self clampRangeToText:_selectedRange];
if (_markedRange.location != NSNotFound)
{
_markedRange = NSMakeRange(MIN(_markedRange.location, _text.length), _markedRange.length);
}
}
- (void) setSelection:(int)start :(int)end{
_selectedRange = NSMakeRange(start, end - start);
if (end < start)
{
auto temp = start;
start = end;
end = temp;
}
auto length = (int)_text.length;
start = MAX(0, MIN(start, length));
end = MAX(start, MIN(end, length));
_selectedRange = NSMakeRange((NSUInteger)start, (NSUInteger)(end - start));
}
- (void) resetInputMethod{
auto parent = _parent.tryGet();
if(parent != nullptr && parent->InputMethod->IsActive()){
parent->InputMethod->Client->SetPreeditText(nullptr);
}
_markedRange = NSMakeRange(_selectedRange.location, 0);
if([self inputContext]) {
[[self inputContext] discardMarkedText];
}
}
- (void) setCursorRect:(AvnRect)rect{

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

@ -628,14 +628,18 @@
return GetNSStringAndRelease(automationPeer->GetAutomationId());
}
- (IAvnAutomationPeer* _Nonnull) automationPeer
- (IAvnAutomationPeer* _Nullable) automationPeer
{
auto parent = _parent.tryGet();
if (parent && _automationPeer == nullptr)
{
_automationPeer = parent->BaseEvents->GetAutomationPeer();
_automationNode = new AvnAutomationNode(self);
_automationPeer->SetNode(_automationNode);
auto peer = parent->BaseEvents->GetAutomationPeer();
if (peer != nullptr)
{
_automationPeer = peer;
_automationNode = new AvnAutomationNode(self);
_automationPeer->SetNode(_automationNode);
}
}
return _automationPeer;

46
native/Avalonia.Native/src/OSX/PlatformSettings.mm

@ -1,12 +1,19 @@
#include "common.h"
#include "AvnString.h"
@interface CocoaThemeObserver : NSObject
-(id)initWithCallback:(IAvnActionCallback *)callback;
@end
@interface CocoaLocaleObserver : NSObject
-(id)initWithCallback:(IAvnActionCallback *)callback;
-(void)localeDidChange:(NSNotification *)notification;
@end
class PlatformSettings : public ComSingleObject<IAvnPlatformSettings, &IID_IAvnPlatformSettings>
{
CocoaThemeObserver* observer;
CocoaLocaleObserver* localeObserver;
public:
FORWARD_IUNKNOWN()
@ -58,6 +65,28 @@ public:
[[NSApplication sharedApplication] addObserver:observer forKeyPath:@"effectiveAppearance" options:NSKeyValueObservingOptionNew context:nil];
}
}
virtual HRESULT GetPreferredLanguage(IAvnString** ret) override
{
@autoreleasepool
{
if (ret == nullptr)
return E_POINTER;
auto language = [[NSLocale preferredLanguages] firstObject];
*ret = language == nil ? nullptr : CreateAvnString(language);
return S_OK;
}
}
virtual void RegisterLanguageChange(IAvnActionCallback *callback) override
{
localeObserver = [[CocoaLocaleObserver alloc] initWithCallback: callback];
[[NSNotificationCenter defaultCenter] addObserver:localeObserver
selector:@selector(localeDidChange:)
name:NSCurrentLocaleDidChangeNotification
object:nil];
}
private:
unsigned int to_argb(NSColor* color)
@ -107,6 +136,23 @@ private:
}
@end
@implementation CocoaLocaleObserver
{
ComPtr<IAvnActionCallback> _callback;
}
- (id) initWithCallback:(IAvnActionCallback *)callback {
self = [super init];
if (self) {
_callback = callback;
}
return self;
}
- (void)localeDidChange:(NSNotification *)notification {
_callback->Run();
}
@end
extern IAvnPlatformSettings* CreatePlatformSettings()
{
return new PlatformSettings();

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

@ -43,6 +43,17 @@ public:
return WindowBaseImpl::Show(activate, true);
}
virtual HRESULT SetHitTestVisible(bool value) override
{
START_COM_CALL;
@autoreleasepool
{
[Window setIgnoresMouseEvents:!value];
return S_OK;
}
}
virtual bool ShouldTakeFocusOnShow() override
{
auto parent = Parent.tryGet();

42
native/Avalonia.Native/src/OSX/StorageProvider.mm

@ -165,6 +165,17 @@ public:
return nullptr;
}
static int GetSelectedFilterIndex(NSSavePanel* _Nonnull panel)
{
if (panel.accessoryView != nil)
{
auto popup = [panel.accessoryView viewWithTag:kFileTypePopupTag];
if ([popup isKindOfClass:[NSPopUpButton class]])
return (int)[(NSPopUpButton*)popup indexOfSelectedItem];
}
return -1;
}
virtual void SelectFolderDialog (IAvnTopLevel* parentTopLevel,
IAvnSystemDialogEvents* events,
@ -181,6 +192,7 @@ public:
panel.canChooseDirectories = true;
panel.canCreateDirectories = true;
panel.canChooseFiles = false;
panel.treatsFilePackagesAsDirectories = true;
if(title != nullptr)
{
@ -270,30 +282,32 @@ public:
auto parentWindow = GetEffectiveNSWindow(parentTopLevel);
auto handler = ^(NSModalResponse result) {
auto selectedIndex = GetSelectedFilterIndex(panel);
if(result == NSFileHandlingPanelOKButton)
{
auto urls = [panel URLs];
if(urls.count > 0)
{
auto uriStrings = CreateAvnStringArray(urls);
ownedEvents->OnCompleted(uriStrings);
ownedEvents->OnCompletedWithFilter(uriStrings, selectedIndex);
[panel orderOut:panel];
if (parentWindow != nullptr)
{
[parentWindow makeKeyAndOrderFront:parentWindow];
}
return;
}
}
ownedEvents->OnCompleted(nullptr);
ownedEvents->OnCompletedWithFilter(nullptr, selectedIndex);
};
if (parentWindow != nullptr)
{
[panel beginSheetModalForWindow:parentWindow completionHandler:handler];
@ -304,7 +318,7 @@ public:
}
}
}
virtual void SaveFileDialog (IAvnTopLevel* parentTopLevel,
IAvnSystemDialogEvents* events,
const char* title,
@ -340,15 +354,7 @@ public:
auto parentWindow = GetEffectiveNSWindow(parentTopLevel);
auto handler = ^(NSModalResponse result) {
int selectedIndex = -1;
if (panel.accessoryView != nil)
{
auto popup = [panel.accessoryView viewWithTag:kFileTypePopupTag];
if ([popup isKindOfClass:[NSPopUpButton class]])
{
selectedIndex = (int)[(NSPopUpButton*)popup indexOfSelectedItem];
}
}
auto selectedIndex = GetSelectedFilterIndex(panel);
if(result == NSFileHandlingPanelOKButton)
{

51
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -21,6 +21,24 @@
@class AutoFitContentView;
// Activates the app, but defers until the run loop is running if it isn't yet.
// Windows are shown (and the app activated) from OnFrameworkInitializationCompleted,
// which runs before -[NSApplication run]. Activating that early yields a "degraded"
// activation: the app looks frontmost but the WindowServer never does a real
// become-active, which leaves the out-of-process file picker unable to receive
// clicks in its file list/sidebar until a manual app switch. Deferring makes the first
// activation a clean transition.
static void ActivateApplication() {
if ([NSApp isRunning]) {
[NSApp activateIgnoringOtherApps:YES];
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[NSApp activateIgnoringOtherApps:YES];
});
}
}
WindowBaseImpl::~WindowBaseImpl() {
View = nullptr;
Window = nullptr;
@ -99,7 +117,7 @@ HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) {
[Window orderFront:Window];
[Window makeKeyAndOrderFront:Window];
[Window makeFirstResponder:View];
[NSApp activateIgnoringOtherApps:YES];
ActivateApplication();
} else {
[Window orderFront:Window];
}
@ -177,7 +195,7 @@ HRESULT WindowBaseImpl::Activate() {
@autoreleasepool {
if (Window != nullptr) {
[Window makeKeyAndOrderFront:nil];
[NSApp activateIgnoringOtherApps:YES];
ActivateApplication();
}
}
@ -329,6 +347,21 @@ HRESULT WindowBaseImpl::BeginMoveDrag() {
@autoreleasepool {
auto lastEvent = [View lastMouseDownEvent];
if (lastEvent == nullptr) {
// A press that begins inside an embedded native view (for example a webview hosted
// through NativeControlHost) is consumed by that view and never delivered to the
// Avalonia view, so no mouse-down is recorded; fall back to the event the
// application is tracking right now, provided it is a left-button press or drag
// that belongs to this window.
auto currentEvent = [NSApp currentEvent];
if (currentEvent != nullptr && [currentEvent window] == Window &&
([currentEvent type] == NSEventTypeLeftMouseDown ||
[currentEvent type] == NSEventTypeLeftMouseDragged)) {
lastEvent = currentEvent;
}
}
if (lastEvent == nullptr) {
return S_OK;
}
@ -432,11 +465,21 @@ void WindowBaseImpl::CleanNSWindow() {
}
void WindowBaseImpl::CreateNSWindow(bool usePanel) {
// Anchor the content rect to the primary screen's origin. It's usually at (0,0), but can be offset while the
// display server is mid-reconfiguration. This ensures the window is always on a proper screen.
// See https://github.com/AvaloniaUI/Avalonia/issues/18895
NSPoint origin = NSZeroPoint;
NSArray<NSScreen*>* screens = [NSScreen screens];
if (screens.count > 0)
origin = screens.firstObject.frame.origin;
NSRect contentRect = NSRect { origin.x, origin.y, lastSize };
if (usePanel) {
Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless];
Window = [[AvnPanel alloc] initWithParent:this contentRect:contentRect styleMask:NSWindowStyleMaskBorderless];
[Window setHidesOnDeactivate:false];
} else {
Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless];
Window = [[AvnWindow alloc] initWithParent:this contentRect:contentRect styleMask:NSWindowStyleMaskBorderless];
}
}

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

@ -17,7 +17,7 @@ struct IAvnAutomationPeer;
-(void) showAppMenuOnly;
-(void) showWindowMenuWithAppMenu;
-(void) applyMenu:(AvnMenu* _Nullable)menu;
-(IAvnAutomationPeer* _Nonnull) automationPeer;
-(IAvnAutomationPeer* _Nullable) automationPeer;
-(double) getExtendedTitleBarHeight;
-(void) setIsExtended:(bool)value;

101
native/Avalonia.Native/src/OSX/automation.mm

@ -96,7 +96,7 @@
case AutomationEdit: return NSAccessibilityTextFieldRole;
case AutomationHyperlink: return NSAccessibilityLinkRole;
case AutomationImage: return NSAccessibilityImageRole;
case AutomationListItem: return NSAccessibilityGroupRole;
case AutomationListItem: return NSAccessibilityRowRole;
case AutomationList: return NSAccessibilityListRole;
case AutomationMenu: return NSAccessibilityMenuBarRole;
case AutomationMenuBar: return NSAccessibilityMenuBarRole;
@ -141,6 +141,7 @@
auto controlType = _peer->GetAutomationControlType();
switch (controlType) {
case AutomationList: return @"AXContentList";
case AutomationListItem: return NSAccessibilityTableRowSubrole;
}
auto landmarkType = _peer->GetLandmarkType();
@ -259,20 +260,36 @@
- (void)setAccessibilityValue:(id)newValue
{
if (_peer->IsValueProvider())
if (!_peer->IsEnabled())
return;
if (_peer->IsValueProvider() && !_peer->ValueProvider_IsReadOnly())
{
if (newValue == nil)
_peer->ValueProvider_SetValue(nil);
else if ([newValue isKindOfClass:[NSString class]])
_peer->ValueProvider_SetValue([(NSString*)newValue UTF8String]);
}
else if (_peer->IsRangeValueProvider())
else if (_peer->IsRangeValueProvider() && !_peer->RangeValueProvider_IsReadOnly())
{
if ([newValue isKindOfClass:[NSNumber class]])
_peer->RangeValueProvider_SetValue([(NSNumber*)newValue doubleValue]);
}
}
- (BOOL)accessibilityIsAttributeSettable:(NSAccessibilityAttributeName)attribute
{
if ([attribute isEqualToString:NSAccessibilityValueAttribute])
{
if (_peer->IsValueProvider())
return !_peer->ValueProvider_IsReadOnly();
if (_peer->IsRangeValueProvider())
return !_peer->RangeValueProvider_IsReadOnly();
return NO;
}
return [super accessibilityIsAttributeSettable:attribute];
}
- (id)accessibilityMinValue
{
if (_peer->IsRangeValueProvider())
@ -357,6 +374,20 @@
return [AvnAccessibilityElement acquire:rootPeer];
}
- (id)accessibilityHorizontalScrollBar
{
if (_peer == nullptr)
return nil;
return [AvnAccessibilityElement acquire:_peer->ScrollProvider_GetHorizontalScrollBar()];
}
- (id)accessibilityVerticalScrollBar
{
if (_peer == nullptr)
return nil;
return [AvnAccessibilityElement acquire:_peer->ScrollProvider_GetVerticalScrollBar()];
}
- (BOOL)isAccessibilityExpanded
{
if (!_peer->IsExpandCollapseProvider())
@ -366,7 +397,7 @@
- (void)setAccessibilityExpanded:(BOOL)accessibilityExpanded
{
if (!_peer->IsExpandCollapseProvider())
if (!_peer->IsExpandCollapseProvider() || !_peer->IsEnabled())
return;
if (accessibilityExpanded)
_peer->ExpandCollapseProvider_Expand();
@ -376,13 +407,18 @@
- (BOOL)accessibilityPerformPress
{
if (!_peer->IsEnabled())
return NO;
if (_peer->IsInvokeProvider())
{
_peer->InvokeProvider_Invoke();
}
else if (_peer->IsExpandCollapseProvider())
{
_peer->ExpandCollapseProvider_Expand();
if (_peer->ExpandCollapseProvider_GetIsExpanded())
_peer->ExpandCollapseProvider_Collapse();
else
_peer->ExpandCollapseProvider_Expand();
}
else if (_peer->IsToggleProvider())
{
@ -393,7 +429,7 @@
- (BOOL)accessibilityPerformIncrement
{
if (!_peer->IsRangeValueProvider())
if (!_peer->IsRangeValueProvider() || _peer->RangeValueProvider_IsReadOnly() || !_peer->IsEnabled())
return NO;
auto value = _peer->RangeValueProvider_GetValue();
value += _peer->RangeValueProvider_GetSmallChange();
@ -403,7 +439,7 @@
- (BOOL)accessibilityPerformDecrement
{
if (!_peer->IsRangeValueProvider())
if (!_peer->IsRangeValueProvider() || _peer->RangeValueProvider_IsReadOnly() || !_peer->IsEnabled())
return NO;
auto value = _peer->RangeValueProvider_GetValue();
value -= _peer->RangeValueProvider_GetSmallChange();
@ -413,7 +449,7 @@
- (BOOL)accessibilityPerformShowMenu
{
if (!_peer->IsExpandCollapseProvider())
if (!_peer->IsExpandCollapseProvider() || !_peer->IsEnabled())
return NO;
_peer->ExpandCollapseProvider_Expand();
return YES;
@ -445,11 +481,22 @@
return NO;
}
- (void)setAccessibilitySelected:(BOOL)accessibilitySelected
{
if (!_peer->IsSelectionItemProvider() || !_peer->IsEnabled())
return;
if (accessibilitySelected)
_peer->SelectionItemProvider_AddToSelection();
else
_peer->SelectionItemProvider_RemoveFromSelection();
}
- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector
{
if (selector == @selector(setAccessibilityValue:))
{
return _peer->IsValueProvider() || _peer->IsRangeValueProvider();
return (_peer->IsValueProvider() && !_peer->ValueProvider_IsReadOnly()) ||
(_peer->IsRangeValueProvider() && !_peer->RangeValueProvider_IsReadOnly());
}
else if (selector == @selector(accessibilityPerformShowMenu))
{
@ -463,9 +510,16 @@
{
return _peer->IsInvokeProvider() || _peer->IsExpandCollapseProvider() || _peer->IsToggleProvider();
}
else if (selector == @selector(setAccessibilitySelected:))
{
return _peer->IsSelectionItemProvider();
}
else if (selector == @selector(accessibilityPerformIncrement) ||
selector == @selector(accessibilityPerformDecrement) ||
selector == @selector(accessibilityMinValue) ||
selector == @selector(accessibilityPerformDecrement))
{
return _peer->IsRangeValueProvider() && !_peer->RangeValueProvider_IsReadOnly();
}
else if (selector == @selector(accessibilityMinValue) ||
selector == @selector(accessibilityMaxValue))
{
return _peer->IsRangeValueProvider();
@ -501,8 +555,31 @@
if (_children)
[changed addObjectsFromArray:_children];
/*
For future reference, upon testing with a sample SwiftUI app:
1) Containers vanish. VStack/HStack don't appear in the accessibility tree at all,
only real elements (Text, Button, List) do, parented to one root AXHostingView.
2) Changes post on the nearest real element, not the container.
Toggling a child two VStack/HStack levels deep fired AXLayoutChanged on the AXHostingView
(the nearest real element), never on the hidden containers.
3) Real controls get their own notification. The List (AXOutline) posted AXRowCountChanged on itself.
Apple never posts a structural notification on a non-accessibility container.
It targets the nearest element that's actually in the tree.
That's exactly what the code below does (templated parent, else walk to the nearest exposed ancestor).
*/
id target = [AvnAccessibilityElement acquire:_peer->GetTemplatedParent()];
if (target == nil)
target = self;
while ([target isKindOfClass:[AvnAccessibilityElement class]] && ![(AvnAccessibilityElement*)target isAccessibilityElement])
target = [(AvnAccessibilityElement*)target accessibilityParent];
NSAccessibilityPostNotificationWithUserInfo(
self,
target,
NSAccessibilityLayoutChangedNotification,
@{ NSAccessibilityUIElementsKey: [changed allObjects]});
}

15
native/Avalonia.Native/src/OSX/menu.mm

@ -101,20 +101,29 @@ NSMenuItem* AvnAppMenuItem::GetNative()
HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu)
{
START_COM_CALL;
@autoreleasepool
{
if(menu != nullptr)
{
auto nsMenu = dynamic_cast<AvnAppMenu*>(menu)->GetNative();
[_native setSubmenu: nsMenu];
// Parity with -[NSMenu setSubmenu:forItem:]: without submenuAction: a click is dispatched to
// didSelectItem: and dismisses the menu instead of opening the submenu.
[_native setTarget: nil];
[_native setAction: @selector(submenuAction:)];
}
else
{
[_native setSubmenu: nullptr];
// The item is reused, so put its own action back.
[_native setTarget: _native];
[_native setAction: @selector(didSelectItem:)];
}
return S_OK;
}
}

33
native/Avalonia.Native/src/OSX/metal.mm

@ -181,12 +181,13 @@ class AvnMetalRenderSession : public ComSingleObject<IAvnMetalRenderingSession,
CAMetalLayer* _layer;
AvnPixelSize _size;
double _scaling;
bool _presentWithTransaction;
public:
FORWARD_IUNKNOWN()
AvnMetalRenderSession(AvnMetalDevice* device, CAMetalLayer* layer, id <CAMetalDrawable> drawable, const AvnPixelSize &size, double scaling)
AvnMetalRenderSession(AvnMetalDevice* device, CAMetalLayer* layer, id <CAMetalDrawable> drawable, const AvnPixelSize &size, double scaling, bool presentWithTransaction)
: _drawable(drawable), _size(size), _scaling(scaling), _queue(device->queue),
_texture([drawable texture]) {
_texture([drawable texture]), _presentWithTransaction(presentWithTransaction) {
_layer = layer;
}
@ -207,8 +208,19 @@ public:
{
START_ARP_CALL;
auto buffer = [_queue commandBuffer];
[buffer presentDrawable: _drawable];
[buffer commit];
if(_presentWithTransaction)
{
[buffer commit];
[buffer waitUntilScheduled];
[_drawable present];
// Restore the default asynchronous presentation for the off-thread render loop.
_layer.presentsWithTransaction = NO;
}
else
{
[buffer presentDrawable: _drawable];
[buffer commit];
}
}
};
@ -230,7 +242,8 @@ public:
HRESULT BeginDrawing(IAvnMetalRenderingSession **ret) override {
START_COM_ARP_CALL;
if([NSThread isMainThread])
bool onMainThread = [NSThread isMainThread];
if(onMainThread)
{
// Flush all existing rendering
auto buffer = [_device->queue commandBuffer];
@ -240,15 +253,21 @@ public:
_scaling= PendingScaling;
CGSize layerSize = {(CGFloat)_size.Width, (CGFloat)_size.Height};
[CATransaction begin];
[CATransaction setDisableActions:YES];
[_layer setDrawableSize: layerSize];
_layer.presentsWithTransaction = YES;
[CATransaction commit];
}
auto drawable = [_layer nextDrawable];
if(drawable == nil)
{
ret = nil;
if(onMainThread)
_layer.presentsWithTransaction = NO;
*ret = nullptr;
return E_FAIL;
}
*ret = new AvnMetalRenderSession(_device, _layer, drawable, _size, _scaling);
*ret = new AvnMetalRenderSession(_device, _layer, drawable, _size, _scaling, onMainThread);
return 0;
}
};

6
nukebuild/ApiDiffHelper.cs

@ -399,7 +399,11 @@ public static class ApiDiffHelper
{
var packageSource = new PackageSource(NightlyFeedUri) { ProtocolVersion = 3 };
var repository = Repository.Factory.GetCoreV3(packageSource);
var findPackageByIdResource = await repository.GetResourceAsync<FindPackageByIdResource>();
var findPackageByIdResource =
await repository.GetResourceAsync<FindPackageByIdResource>() ??
throw new InvalidOperationException($"{nameof(FindPackageByIdResource)} not found on {packageSource}");
return new NuGetDownloadContext(packageSource, findPackageByIdResource);
}

40
nukebuild/Build.cs

@ -43,6 +43,9 @@ partial class Build : NukeBuild
[NuGetPackage("dotnet-ilrepack", "ILRepackTool.dll", Framework = "net8.0")]
Tool IlRepackTool;
[NuGetPackage("CycloneDX", "CycloneDX.dll", Framework = "net10.0")]
Tool CycloneDxTool;
protected override void OnBuildInitialized()
{
Parameters = new BuildParameters(this, ScheduledTargets.Contains(BuildToNuGetCache));
@ -227,6 +230,10 @@ partial class Build : NukeBuild
{
tfm = "net8.0";
}
if (tfm == "$(AvsCurrentWindowsTargetFramework)")
{
tfm = "net10.0-windows";
}
if (tfm.StartsWith("net4")
&& (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
@ -280,6 +287,9 @@ partial class Build : NukeBuild
RunCoreTest("Avalonia.Headless.XUnit.PerAssembly.UnitTests");
RunCoreTest("Avalonia.Headless.XUnit.PerTest.UnitTests");
RunCoreTest("Avalonia.Themes.Fluent2.UnitTests");
if (Parameters.IsRunningOnWindows)
RunCoreTest("Avalonia.UnitTests.WpfCompare");
});
Target RunRenderTests => _ => _
@ -309,7 +319,9 @@ partial class Build : NukeBuild
});
Target ZipFiles => _ => _
.After(CreateNugetPackages, Compile, RunCoreLibsTests, Package)
// CreateSbom embeds the SBOM into each .nupkg in NugetRoot, so it must run before we zip
// that directory - otherwise the zipped NuGet artifacts would omit the embedded SBOM.
.After(CreateNugetPackages, Compile, RunCoreLibsTests, Package, CreateSbom)
.Executes(() =>
{
var data = Parameters;
@ -341,6 +353,20 @@ partial class Build : NukeBuild
Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.snupkg");
});
Target CreateSbom => _ => _
.DependsOn(CreateNugetPackages)
.Executes(() =>
{
SbomGenerator.Generate(
CycloneDxTool,
RootDirectory,
Parameters.NugetRoot,
Parameters.NugetIntermediateRoot,
RootDirectory / "nukebuild" / "numerge.json",
Parameters.SbomRoot,
Parameters.Version);
});
Target DownloadApiBaselinePackages => _ => _
.DependsOn(CreateNugetPackages)
.Executes(async () =>
@ -410,12 +436,14 @@ partial class Build : NukeBuild
Target CiAzureOSX => _ => _
.DependsOn(Package)
.DependsOn(ZipFiles);
.DependsOn(ZipFiles)
.DependsOn(CreateSbom);
Target CiAzureWindows => _ => _
.DependsOn(Package)
.DependsOn(VerifyXamlCompilation)
.DependsOn(ZipFiles);
.DependsOn(ZipFiles)
.DependsOn(CreateSbom);
Target BuildToNuGetCache => _ => _
.DependsOn(CreateNugetPackages)
@ -429,12 +457,10 @@ partial class Build : NukeBuild
foreach (var path in Parameters.NugetRoot.GlobFiles("*.nupkg"))
{
var packageId = SbomGenerator.ReadPackageId(path);
using var f = File.Open(path.ToString(), FileMode.Open, FileAccess.Read);
using var zip = new ZipArchive(f, ZipArchiveMode.Read);
var nuspecEntry = zip.Entries.First(e => e.FullName.EndsWith(".nuspec") && e.FullName == e.Name);
var packageId = XDocument.Load(nuspecEntry.Open()).Document.Root
.Elements().First(x => x.Name.LocalName == "metadata")
.Elements().First(x => x.Name.LocalName == "id").Value;
var packagePath = Path.Combine(
globalPackagesFolder,

2
nukebuild/BuildParameters.cs

@ -60,6 +60,7 @@ public partial class Build
public AbsolutePath ArtifactsDir { get; }
public AbsolutePath NugetIntermediateRoot { get; }
public AbsolutePath NugetRoot { get; }
public AbsolutePath SbomRoot { get; }
public AbsolutePath ZipRoot { get; }
public AbsolutePath TestResultsRoot { get; }
public string DirSuffix { get; }
@ -139,6 +140,7 @@ public partial class Build
ArtifactsDir = RootDirectory / "artifacts";
NugetRoot = ArtifactsDir / "nuget";
NugetIntermediateRoot = RootDirectory / "build-intermediate" / "nuget";
SbomRoot = ArtifactsDir / "sbom";
ZipRoot = ArtifactsDir / "zip";
TestResultsRoot = ArtifactsDir / "test-results";
BuildDirs = RootDirectory.GlobDirectories("**/bin")

740
nukebuild/SbomGenerator.cs

@ -0,0 +1,740 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Nuke.Common.IO;
using Nuke.Common.Tooling;
using static Serilog.Log;
// Generates one CycloneDX SBOM per published NuGet package, using the already-restored
// solution (obj/project.assets.json) so component versions are the ones actually resolved
// into the build, rather than the floating PackageReference ranges GitHub's own dependency
// graph reports.
//
// Numerge (see numerge.json) merges several source projects' packed output into a handful of
// the final NuGet packages (e.g. "Avalonia" absorbs Avalonia.Base, Avalonia.Controls, the
// build tasks/analyzers, etc). That merge happens on the built .nupkg files, not via MSBuild
// ProjectReferences, so it can't be discovered by pointing CycloneDX at a single project with
// -rs/--recursive. Instead this compares the intermediate and final package sets to work out
// which source projects were folded into which final package, generates a BOM per constituent
// project, and unions their components (de-duplicated by purl) into one BOM per final package.
public static class SbomGenerator
{
public static void Generate(
Tool cycloneDx,
AbsolutePath rootDirectory,
AbsolutePath nugetRoot,
AbsolutePath nugetIntermediateRoot,
AbsolutePath numergeConfigPath,
AbsolutePath outputDirectory,
string version)
{
outputDirectory.CreateOrCleanDirectory();
// Read each final package's id exactly once here (opening/unzipping a nupkg to parse its
// nuspec isn't free) and reuse the id->path map when locating each package's own nupkg
// below, rather than re-scanning every nupkg per final package.
var finalPackagePathsById = nugetRoot.GlobFiles("*.nupkg")
.GroupBy(p => ReadPackageId((string)p))
.ToDictionary(g => g.Key, g => g.First());
var finalPackageIds = finalPackagePathsById.Keys.ToHashSet();
var intermediatePackageIds = nugetIntermediateRoot.GlobFiles("*.nupkg")
.Select(p => ReadPackageId((string)p)).Distinct();
var numerge = Numerge.MergeConfiguration.LoadFile(numergeConfigPath);
var explicitParentByChild = numerge.Packages
.SelectMany(p => p.Merge.Select(c => (Parent: p.Id, Child: c.Id)))
.ToDictionary(x => x.Child, x => x.Parent);
var mergeAllParent = numerge.Packages.FirstOrDefault(p => p.MergeAll)?.Id;
// Every final package starts out as its own sole constituent; leftover intermediate
// packages (ones that never shipped standalone) get assigned to whichever final
// package absorbed them, per numerge.json.
var constituentProjectIdsByFinalId = finalPackageIds.ToDictionary(id => id, id => new List<string> { id });
foreach (var id in intermediatePackageIds.Where(id => !finalPackageIds.Contains(id)))
{
var owner = explicitParentByChild.TryGetValue(id, out var explicitOwner) ? explicitOwner : mergeAllParent;
if (owner is not null && constituentProjectIdsByFinalId.TryGetValue(owner, out var siblings))
siblings.Add(id);
else
Warning($"SBOM: couldn't determine which published package absorbs intermediate package '{id}' - it will be missing from all generated SBOMs.");
}
foreach (var (finalId, projectIds) in constituentProjectIdsByFinalId)
GenerateForPackage(cycloneDx, rootDirectory, finalPackagePathsById, outputDirectory, version, finalId, projectIds);
}
static void GenerateForPackage(Tool cycloneDx, AbsolutePath rootDirectory,
IReadOnlyDictionary<string, AbsolutePath> finalPackagePathsById,
AbsolutePath outputDirectory, string version, string finalId, List<string> projectIds)
{
JsonObject? merged = null;
var seenComponentKeys = new HashSet<string>();
var scannedProjectDirs = new List<AbsolutePath>();
foreach (var projectId in projectIds)
{
var project = rootDirectory.GlobFiles($"src/**/{projectId}.csproj")
.Concat(rootDirectory.GlobFiles($"packages/**/{projectId}.csproj"))
.FirstOrDefault();
if (project is null)
{
Warning($"SBOM: couldn't locate source project for '{projectId}', skipping it in the SBOM for '{finalId}'.");
continue;
}
scannedProjectDirs.Add(project.Parent);
var tempBom = outputDirectory / $"_{projectId}.tmp.json";
cycloneDx(
$"\"{project}\" -o \"{outputDirectory}\" -fn \"{tempBom.Name}\" -F Json -dpr -ed -sn \"{finalId}\" -sv \"{version}\"",
workingDirectory: rootDirectory);
var doc = JsonNode.Parse(File.ReadAllText(tempBom))!.AsObject();
File.Delete(tempBom);
var components = doc["components"]?.AsArray() ?? new JsonArray();
if (merged is null)
{
merged = doc;
foreach (var component in components)
seenComponentKeys.Add(ComponentKey(component));
}
else
{
var target = merged["components"]?.AsArray() ?? (JsonArray)(merged["components"] = new JsonArray());
foreach (var component in components)
{
if (seenComponentKeys.Add(ComponentKey(component)))
target.Add(component!.DeepClone());
}
// Every constituent project's own -sn/-sv override makes its root component (and
// therefore its dependency-graph "ref") identical to the final package's, so merging
// by ref correctly unions all constituents' dependsOn edges onto that shared root
// instead of silently keeping only the first project's edges.
MergeDependencyGraph(merged, doc["dependencies"]?.AsArray() ?? new JsonArray());
}
}
if (merged is null)
{
Warning($"SBOM: no source projects could be scanned for '{finalId}', no SBOM was generated for it.");
return;
}
// cyclonedx-dotnet only sees the MSBuild/NuGet graph. Some projects also bundle a
// Bun/npm-built webapp directly into their published package (e.g. Avalonia.Browser's
// staticwebassets, Avalonia.DesignerSupport's embedded previewer) - scan those separately
// so their shipped JS dependencies aren't silently absent from the SBOM.
var rootRef = merged["metadata"]?["component"]?["bom-ref"]?.GetValue<string>();
foreach (var projectDir in scannedProjectDirs)
AddNpmComponents(merged, seenComponentKeys, projectDir, rootRef);
// The final .nupkg carries the authoritative publisher/license/repository metadata and the
// actual shipped binaries; use it to flesh out the thin root component cyclonedx-dotnet
// emits and to verify nothing ships that the dependency scan didn't already account for.
var finalNupkg = finalPackagePathsById.GetValueOrDefault(finalId);
if (finalNupkg is not null)
{
var nuspec = ReadNuspecMetadata((string)finalNupkg);
EnrichRootComponent(merged, nuspec);
AddPackageContentComponents(merged, seenComponentKeys, (string)finalNupkg, finalId, projectIds, nuspec);
}
else
{
Warning($"SBOM: couldn't find the built .nupkg for '{finalId}' - root metadata and package-content verification were skipped.");
}
// cyclonedx-dotnet leaves dependsOn edges pointing at packages it excluded as dev
// dependencies (e.g. analyzers stripped by -ed), which dangle once the component is gone
// (upstream bug CycloneDX/cyclonedx-dotnet#761, still reproducing in 6.2.0). Drop those so
// the graph only references components actually present in the SBOM.
PruneDanglingDependencyEdges(merged);
var sbomJson = merged.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(outputDirectory / $"{finalId}.{version}.cdx.json", sbomJson);
// Embed the SBOM inside the shipped .nupkg so it travels with the package, in addition to
// the standalone copy written above (which CI publishes as the SBOM artifact) - belt and
// suspenders: consumers who only ever see the package still get its bill of materials.
if (finalNupkg is not null)
EmbedSbomInPackage(finalNupkg, sbomJson);
}
// The path inside the .nupkg where the CycloneDX SBOM is embedded. Mirrors the _manifest/
// layout Microsoft.Sbom.Targets uses for its SPDX manifest, but keeps CycloneDX's recognised
// *.cdx.json filename so tools that scan for that pattern still find it once unpacked.
const string EmbeddedSbomEntryPath = "_manifest/cyclonedx/bom.cdx.json";
// Adds the generated SBOM as a new part inside the shipped package. Must run before the package
// is signed - a NuGet signature covers the whole archive, so adding a part afterwards would
// invalidate it. That holds here: these packages are signed server-side by nuget.org on push,
// which happens after this build step.
static void EmbedSbomInPackage(AbsolutePath nupkgPath, string sbomJson)
{
using var file = File.Open(nupkgPath, FileMode.Open, FileAccess.ReadWrite);
using var zip = new ZipArchive(file, ZipArchiveMode.Update);
if (zip.Entries.Any(e => e.FullName.EndsWith(".signature.p7s", StringComparison.OrdinalIgnoreCase)))
{
Warning($"SBOM: '{nupkgPath.Name}' is already signed - skipping embed so its signature stays valid.");
return;
}
// Re-embedding (e.g. a re-run over the same output) should replace, not stack duplicates.
zip.GetEntry(EmbeddedSbomEntryPath)?.Delete();
using (var entryStream = zip.CreateEntry(EmbeddedSbomEntryPath).Open())
using (var writer = new StreamWriter(entryStream))
writer.Write(sbomJson);
EnsureJsonContentTypeRegistered(zip);
}
// A .nupkg is an OPC package: every part's extension must be declared in [Content_Types].xml or
// strict OPC readers - including NuGet's own signature verification - reject the package. The
// SBOM is a .json part, so register that extension before (or as) we add it.
static void EnsureJsonContentTypeRegistered(ZipArchive zip)
{
const string contentTypesEntryName = "[Content_Types].xml";
XNamespace ns = "http://schemas.openxmlformats.org/package/2006/content-types";
var entry = zip.GetEntry(contentTypesEntryName);
if (entry is null)
return; // not a well-formed OPC package; don't fabricate one
XDocument doc;
using (var read = entry.Open())
doc = XDocument.Load(read);
var alreadyRegistered = doc.Root!.Elements(ns + "Default")
.Any(d => string.Equals((string?)d.Attribute("Extension"), "json", StringComparison.OrdinalIgnoreCase));
if (alreadyRegistered)
return;
doc.Root.Add(new XElement(ns + "Default",
new XAttribute("Extension", "json"),
new XAttribute("ContentType", "application/json")));
entry.Delete();
using var write = zip.CreateEntry(contentTypesEntryName).Open();
doc.Save(write);
}
static void PruneDanglingDependencyEdges(JsonObject merged)
{
var deps = merged["dependencies"]?.AsArray();
if (deps is null)
return;
var known = new HashSet<string>();
var rootRef = merged["metadata"]?["component"]?["bom-ref"]?.GetValue<string>();
if (rootRef is not null)
known.Add(rootRef);
foreach (var component in merged["components"]?.AsArray() ?? new JsonArray())
{
if (component?["bom-ref"]?.GetValue<string>() is { } bomRef)
known.Add(bomRef);
if (component?["purl"]?.GetValue<string>() is { } purl)
known.Add(purl);
}
foreach (var node in deps.OfType<JsonObject>())
{
var dependsOn = node["dependsOn"]?.AsArray();
if (dependsOn is null)
continue;
var kept = new JsonArray();
foreach (var edge in dependsOn)
if (known.Contains(edge!.GetValue<string>()))
kept.Add(edge.GetValue<string>());
node["dependsOn"] = kept;
}
}
static void MergeDependencyGraph(JsonObject target, JsonArray incoming)
{
var targetDeps = target["dependencies"]?.AsArray() ?? (JsonArray)(target["dependencies"] = new JsonArray());
var byRef = targetDeps.OfType<JsonObject>().ToDictionary(d => d["ref"]!.GetValue<string>());
foreach (var node in incoming.OfType<JsonObject>())
{
var nodeRef = node["ref"]!.GetValue<string>();
var dependsOn = node["dependsOn"]?.AsArray().Select(x => x!.GetValue<string>()) ?? Enumerable.Empty<string>();
if (!byRef.TryGetValue(nodeRef, out var existing))
{
existing = node.DeepClone().AsObject();
targetDeps.Add(existing);
byRef[nodeRef] = existing;
}
var existingDependsOn = existing["dependsOn"]?.AsArray() ?? (JsonArray)(existing["dependsOn"] = new JsonArray());
var seen = existingDependsOn.Select(x => x!.GetValue<string>()).ToHashSet();
foreach (var dep in dependsOn)
if (seen.Add(dep))
existingDependsOn.Add(dep);
}
}
// Scans <projectDir>/**/webapp/package.json for production "dependencies" (deliberately
// ignoring devDependencies, which never ship) and adds them - plus their transitive
// dependencies, walked through the installed node_modules - as fully-formed components:
// bom-ref, resolved version, license, and dependency-graph edges, matching the shape of the
// NuGet components cyclonedx-dotnet emits so npm packages aren't second-class SBOM entries.
// Versions come from the actually-installed node_modules (same rationale as reading
// project.assets.json rather than trusting floating ranges).
static void AddNpmComponents(JsonObject merged, HashSet<string> seenComponentKeys, AbsolutePath projectDir,
string? rootRef)
{
foreach (string packageJsonPath in projectDir.GlobFiles("**/webapp/package.json"))
{
var packageJson = JsonNode.Parse(File.ReadAllText(packageJsonPath))!.AsObject();
var nodeModules = ((AbsolutePath)packageJsonPath).Parent / "node_modules";
var dependencies = packageJson["dependencies"]?.AsObject() ?? new JsonObject();
// The webapp is bundled into the shipped package, so its direct production
// dependencies are direct dependencies of the final NuGet package.
foreach (var (name, rangeNode) in dependencies)
{
if (IsTypeOnlyPackage(name))
continue;
var purl = AddNpmComponentTree(merged, seenComponentKeys, nodeModules, name,
rangeNode!.GetValue<string>(), nodeModules);
if (rootRef is not null)
AddDependsOn(merged, rootRef, purl);
}
}
}
// Adds the component for (name, range) and, recursively, everything it depends on, returning
// its purl. The component/graph node/subtree are materialised only the first time a purl is
// seen (which also breaks any dependency cycles); repeat encounters just return the purl so
// the caller can still record its own edge to it.
static string AddNpmComponentTree(JsonObject merged, HashSet<string> seenComponentKeys,
AbsolutePath topLevelNodeModules, string name, string declaredRange, AbsolutePath parentNodeModules)
{
var (purl, componentVersion, installedDir) =
ResolveNpmComponent(name, declaredRange, parentNodeModules, topLevelNodeModules);
if (!seenComponentKeys.Add(purl))
return purl;
var installed = installedDir is not null && File.Exists(installedDir / "package.json")
? JsonNode.Parse(File.ReadAllText(installedDir / "package.json"))!.AsObject()
: null;
var component = new JsonObject
{
["type"] = "library",
["bom-ref"] = purl,
["name"] = name,
["version"] = componentVersion,
["purl"] = purl
};
// No hashes: npm's verifiable hashes live in the (binary bun) lockfile, not the installed
// tree, and a hash of the unpacked directory wouldn't be checkable against a registry.
var licenses = installed is null ? null : BuildLicenses(installed);
if (licenses is not null)
component["licenses"] = licenses;
var target = merged["components"]?.AsArray() ?? (JsonArray)(merged["components"] = new JsonArray());
target.Add(component);
var node = new JsonObject { ["ref"] = purl, ["dependsOn"] = new JsonArray() };
(merged["dependencies"]?.AsArray() ?? (JsonArray)(merged["dependencies"] = new JsonArray())).Add(node);
var childDeps = installed?["dependencies"]?.AsObject() ?? new JsonObject();
var childNodeModules = installedDir is not null ? installedDir / "node_modules" : parentNodeModules;
var dependsOn = node["dependsOn"]!.AsArray();
foreach (var (childName, childRange) in childDeps)
{
if (IsTypeOnlyPackage(childName))
continue;
var childPurl = AddNpmComponentTree(merged, seenComponentKeys, topLevelNodeModules, childName,
childRange!.GetValue<string>(), childNodeModules);
dependsOn.Add(childPurl);
}
return purl;
}
// @types/* packages are TypeScript declaration stubs: esbuild strips them at build time, so
// they're never part of the shipped bytes and don't belong in a scope-of-delivery SBOM.
static bool IsTypeOnlyPackage(string name) => name.StartsWith("@types/", StringComparison.Ordinal);
static void AddDependsOn(JsonObject merged, string fromRef, string toPurl)
{
var deps = merged["dependencies"]?.AsArray() ?? (JsonArray)(merged["dependencies"] = new JsonArray());
var node = deps.OfType<JsonObject>().FirstOrDefault(d => d["ref"]?.GetValue<string>() == fromRef);
if (node is null)
{
node = new JsonObject { ["ref"] = fromRef, ["dependsOn"] = new JsonArray() };
deps.Add(node);
}
var dependsOn = node["dependsOn"]?.AsArray() ?? (JsonArray)(node["dependsOn"] = new JsonArray());
if (!dependsOn.Any(x => x!.GetValue<string>() == toPurl))
dependsOn.Add(toPurl);
}
static JsonArray? BuildLicenses(JsonObject installedPackageJson)
{
// Modern npm: "license" is an SPDX id or expression. Legacy: "license"/"licenses" objects.
if (installedPackageJson["license"] is JsonValue licenseValue && licenseValue.TryGetValue(out string? spdx))
{
var licenses = SpdxToLicenses(spdx);
if (licenses is not null)
return licenses;
}
var legacy = (installedPackageJson["license"] as JsonObject)?["type"]?.GetValue<string>()
?? (installedPackageJson["licenses"] as JsonArray)?.OfType<JsonObject>()
.FirstOrDefault()?["type"]?.GetValue<string>();
return legacy is null
? null
: new JsonArray(new JsonObject { ["license"] = new JsonObject { ["name"] = legacy } });
}
// Turns an SPDX string into a CycloneDX licenses array: a single license id becomes a
// {license:{id}} entry, a compound SPDX expression becomes an {expression} entry.
static JsonArray? SpdxToLicenses(string? spdx)
{
if (string.IsNullOrWhiteSpace(spdx))
return null;
var isExpression = spdx.IndexOf(" OR ", StringComparison.Ordinal) >= 0
|| spdx.IndexOf(" AND ", StringComparison.Ordinal) >= 0
|| spdx.IndexOf(" WITH ", StringComparison.Ordinal) >= 0;
return new JsonArray(isExpression
? new JsonObject { ["expression"] = spdx }
: new JsonObject { ["license"] = new JsonObject { ["id"] = spdx } });
}
static (string Purl, string Version, AbsolutePath? InstalledDir) ResolveNpmComponent(
string name, string declaredRange, AbsolutePath parentNodeModules, AbsolutePath topLevelNodeModules)
{
// npm/bun hoist most packages to the top level but may nest a conflicting version under
// the depending package, so prefer the nested copy and fall back to the hoisted one.
var installedDir = new[] { parentNodeModules / name, topLevelNodeModules / name }
.FirstOrDefault(d => File.Exists(d / "package.json"));
if (declaredRange.StartsWith("github:") || declaredRange.StartsWith("git") || declaredRange.Contains("://"))
{
if (TryParseGitHubDependency(declaredRange, out var owner, out var repo, out var reference))
return ($"pkg:github/{owner}/{repo}@{reference}", reference, installedDir);
// A non-GitHub git/URL specifier (GitLab/Bitbucket, a raw tarball URL, ...). We have no
// provider-specific purl for it, so degrade to a generic component - preferring the
// version installed on disk - rather than throwing and failing the whole release over a
// single dependency we can't classify precisely.
var resolvedVersion = installedDir is not null
? JsonNode.Parse(File.ReadAllText(installedDir / "package.json"))!["version"]?.GetValue<string>()
: null;
resolvedVersion ??= declaredRange;
Warning($"SBOM: npm dependency '{name}' uses an unrecognised git/URL specifier '{declaredRange}' - recording it as a generic component with version '{resolvedVersion}'.");
return ($"pkg:generic/{EncodeNpmName(name)}@{resolvedVersion}", resolvedVersion, installedDir);
}
if (installedDir is null)
{
Warning($"SBOM: npm dependency '{name}' isn't installed near {parentNodeModules} - recording its declared range '{declaredRange}' instead of a resolved version.");
return ($"pkg:npm/{EncodeNpmName(name)}@{declaredRange}", declaredRange, null);
}
var installedVersion = JsonNode.Parse(File.ReadAllText(installedDir / "package.json"))!["version"]?.GetValue<string>();
if (installedVersion is null)
{
// package.json without a "version" is valid for private packages; don't let it NRE.
Warning($"SBOM: npm dependency '{name}' installed at {installedDir} has no version in its package.json - recording its declared range '{declaredRange}' instead.");
installedVersion = declaredRange;
}
return ($"pkg:npm/{EncodeNpmName(name)}@{installedVersion}", installedVersion, installedDir);
}
static string EncodeNpmName(string name) => name.StartsWith("@") ? $"%40{name[1..]}" : name;
static bool TryParseGitHubDependency(string spec, out string owner, out string repo, out string reference)
{
owner = repo = "";
var hashIndex = spec.IndexOf('#');
reference = hashIndex >= 0 ? spec[(hashIndex + 1)..] : "HEAD";
var withoutRef = hashIndex >= 0 ? spec[..hashIndex] : spec;
var match = Regex.Match(withoutRef, @"github(?:\.com)?[:/]+([^/]+)/([^/#]+?)(?:\.git)?$");
if (!match.Success)
return false;
owner = match.Groups[1].Value;
repo = match.Groups[2].Value;
return true;
}
// Prefer purl (or bom-ref) as the identity; both encode version. Fall back to name+version so
// distinct versions of the same package aren't collapsed into one, and to a fresh GUID when
// there's nothing identifiable to key on (treating it as unique rather than deduping blindly).
static string ComponentKey(JsonNode? component) =>
component?["purl"]?.GetValue<string>()
?? component?["bom-ref"]?.GetValue<string>()
?? (component?["name"]?.GetValue<string>() is { } name
? $"{name}@{component?["version"]?.GetValue<string>()}"
: Guid.NewGuid().ToString());
// Fills in the root component with the publisher, licence, description and repository details
// from the shipped .nuspec - cyclonedx-dotnet only emits type/name/version, which is far short
// of the manufacturer/provenance information a CRA-facing SBOM is expected to carry.
static void EnrichRootComponent(JsonObject merged, NuspecMetadata meta)
{
var component = merged["metadata"]?["component"]?.AsObject();
if (component is null)
return;
// These are shipped libraries, not applications (cyclonedx-dotnet's default type).
component["type"] = "library";
component["purl"] = $"pkg:nuget/{meta.Id}@{meta.Version}";
if (meta.Description is not null)
component["description"] = meta.Description;
if (meta.Copyright is not null)
component["copyright"] = meta.Copyright;
JsonObject? supplier = null;
if (meta.Authors is not null)
{
component["publisher"] = meta.Authors;
component["author"] = meta.Authors;
supplier = new JsonObject { ["name"] = meta.Authors };
if (meta.ProjectUrl is not null)
supplier["url"] = new JsonArray(meta.ProjectUrl);
component["supplier"] = supplier;
}
var licenses = SpdxToLicenses(meta.LicenseExpression ?? meta.LicenseId);
// A file license has no SPDX id; record it by name rather than emitting an invalid id.
if (licenses is null && meta.LicenseFile is not null)
licenses = new JsonArray(new JsonObject
{ ["license"] = new JsonObject { ["name"] = Path.GetFileName(meta.LicenseFile) } });
if (licenses is not null)
component["licenses"] = licenses;
var externalReferences = new JsonArray();
if (meta.ProjectUrl is not null)
externalReferences.Add(new JsonObject { ["url"] = meta.ProjectUrl, ["type"] = "website" });
if (meta.RepositoryUrl is not null)
externalReferences.Add(new JsonObject { ["url"] = meta.RepositoryUrl, ["type"] = "vcs" });
if (externalReferences.Count > 0)
component["externalReferences"] = externalReferences;
// Also record the manufacturer at the document level (the entity that supplied the BOM).
if (supplier is not null && merged["metadata"] is JsonObject metadata)
metadata["supplier"] = supplier.DeepClone();
}
// Cross-checks what the package actually ships against the components derived from the
// dependency graph. Avalonia's own merged modules (Numerge folds several projects' assemblies
// into one package) are recorded as manufacturer-supplied components with a verifiable SHA-512
// of the shipped bytes; any third-party binary that no restored dependency accounts for is
// added and flagged, so a future bundling regression can't silently escape the SBOM.
//
// These shipped binaries are *constituents* of the package, not dependencies of it: the package
// is made up of them (Component C bundles D and E, in CycloneDX's terms), so they're modelled as
// a CycloneDX "assembly" via a top-level `compositions` entry rather than with a dependsOn edge
// from the root - "contains" and "depends on" are different relationships, and an assembly does
// not imply a dependency. They stay flat top-level components (not nested subcomponents) so their
// per-assembly hashes remain visible to scanners that ignore nested components.
static void AddPackageContentComponents(JsonObject merged, HashSet<string> seenComponentKeys,
string nupkgPath, string finalId, List<string> constituentProjectIds, NuspecMetadata meta)
{
var productNames = new HashSet<string>(constituentProjectIds, StringComparer.OrdinalIgnoreCase) { finalId };
var representedNames = (merged["components"]?.AsArray() ?? new JsonArray())
.Select(c => c?["name"]?.GetValue<string>())
.Where(n => n is not null)
.ToHashSet(StringComparer.OrdinalIgnoreCase)!;
var supplier = meta.Authors is null ? null : new JsonObject { ["name"] = meta.Authors };
var target = merged["components"]?.AsArray() ?? (JsonArray)(merged["components"] = new JsonArray());
var rootRef = merged["metadata"]?["component"]?["bom-ref"]?.GetValue<string>();
// The bundled first-party assemblies share the package's licence; cyclonedx-dotnet only puts
// it on the root component, so carry it onto them too (same derivation as EnrichRootComponent).
var productLicenses = SpdxToLicenses(meta.LicenseExpression ?? meta.LicenseId);
if (productLicenses is null && meta.LicenseFile is not null)
productLicenses = new JsonArray(new JsonObject
{ ["license"] = new JsonObject { ["name"] = Path.GetFileName(meta.LicenseFile) } });
// bom-refs of every shipped binary added below; declared as a complete assembly at the end.
var assemblyRefs = new List<string>();
using var file = File.Open(nupkgPath, FileMode.Open, FileAccess.Read);
using var zip = new ZipArchive(file, ZipArchiveMode.Read);
foreach (var entry in zip.Entries)
{
var path = entry.FullName;
// Reference assemblies under ref/ are compile-time surface, not shipped runtime code;
// the real implementation lives under lib/ and is scanned there.
if (!IsShippedBinary(path) || path.StartsWith("ref/", StringComparison.OrdinalIgnoreCase))
continue;
var bytes = ReadEntry(entry);
var assemblyName = TryReadAssemblyName(bytes, out var assemblyVersion);
var simpleName = assemblyName ?? Path.GetFileNameWithoutExtension(path);
// Third-party binaries already represented by a NuGet/npm component need no duplicate.
if (assemblyName is not null && representedNames.Contains(simpleName))
continue;
var isProduct = productNames.Contains(simpleName)
|| simpleName.StartsWith("Avalonia.", StringComparison.OrdinalIgnoreCase)
|| simpleName.Equals("Avalonia", StringComparison.OrdinalIgnoreCase);
// The package's primary assembly is the root component itself - don't list it as its own subcomponent.
if (isProduct && simpleName.Equals(finalId, StringComparison.OrdinalIgnoreCase))
continue;
var version = assemblyVersion ?? meta.Version;
var bomRef = $"binary:{simpleName}@{version}";
if (!seenComponentKeys.Add(bomRef))
continue;
if (!isProduct)
Warning($"SBOM: package '{finalId}' ships '{path}' ({simpleName}) which no restored dependency accounts for - added from package contents, please verify its provenance.");
var component = new JsonObject
{
["type"] = "library",
["bom-ref"] = bomRef,
["name"] = simpleName,
["version"] = version,
["scope"] = "required",
["hashes"] = new JsonArray(new JsonObject
{
["alg"] = "SHA-512",
["content"] = Convert.ToHexString(SHA512.HashData(bytes))
}),
["properties"] = new JsonArray(new JsonObject
{
["name"] = "avalonia:packagePath",
["value"] = path
})
};
if (isProduct)
{
if (supplier is not null)
component["supplier"] = supplier.DeepClone();
if (productLicenses is not null)
component["licenses"] = productLicenses.DeepClone();
}
target.Add(component);
assemblyRefs.Add(bomRef);
}
// Declare the package's assembly: the root component consists of exactly these bundled
// binaries. We've read every shipped binary out of the .nupkg, so the enumeration is
// complete (third-party dependencies remain in the dependency graph, as they should).
if (assemblyRefs.Count > 0)
{
var assemblies = new JsonArray();
if (rootRef is not null)
assemblies.Add(rootRef);
foreach (var bomRef in assemblyRefs)
assemblies.Add(bomRef);
var compositions = merged["compositions"]?.AsArray()
?? (JsonArray)(merged["compositions"] = new JsonArray());
compositions.Add(new JsonObject
{
["aggregate"] = "complete",
["assemblies"] = assemblies
});
}
}
static bool IsShippedBinary(string path)
{
var ext = Path.GetExtension(path).ToLowerInvariant();
return ext is ".dll" or ".so" or ".dylib" or ".wasm" or ".node" or ".a";
}
static byte[] ReadEntry(ZipArchiveEntry entry)
{
using var stream = entry.Open();
using var ms = new MemoryStream();
stream.CopyTo(ms);
return ms.ToArray();
}
// Returns the managed assembly's simple name (and version), or null for native / non-managed binaries.
static string? TryReadAssemblyName(byte[] bytes, out string? version)
{
version = null;
try
{
using var pe = new PEReader(new MemoryStream(bytes));
if (!pe.HasMetadata)
return null;
var reader = pe.GetMetadataReader();
if (!reader.IsAssembly)
return null;
var assembly = reader.GetAssemblyDefinition();
version = assembly.Version.ToString();
return reader.GetString(assembly.Name);
}
catch (BadImageFormatException)
{
return null;
}
}
class NuspecMetadata
{
public string Id = "";
public string Version = "";
public string? Authors;
public string? LicenseId;
public string? LicenseExpression;
public string? LicenseFile;
public string? ProjectUrl;
public string? RepositoryUrl;
public string? Description;
public string? Copyright;
}
static NuspecMetadata ReadNuspecMetadata(string nupkgPath)
{
using var file = File.Open(nupkgPath, FileMode.Open, FileAccess.Read);
using var zip = new ZipArchive(file, ZipArchiveMode.Read);
var nuspecEntry = zip.Entries.First(e => e.FullName.EndsWith(".nuspec", StringComparison.Ordinal) && e.FullName == e.Name);
using var nuspecStream = nuspecEntry.Open();
var metadata = XDocument.Load(nuspecStream).Root!
.Elements().First(x => x.Name.LocalName == "metadata");
string? Value(string name) => metadata.Elements().FirstOrDefault(x => x.Name.LocalName == name)?.Value;
var license = metadata.Elements().FirstOrDefault(x => x.Name.LocalName == "license");
var repository = metadata.Elements().FirstOrDefault(x => x.Name.LocalName == "repository");
return new NuspecMetadata
{
Id = Value("id") ?? "",
Version = Value("version") ?? "",
Authors = Value("authors"),
// A nuspec <license> is either type="expression" (an SPDX expression) or type="file"
// (a path to a bundled licence file); only the former is a valid SPDX id/expression.
LicenseExpression = license?.Attribute("type")?.Value == "expression" ? license.Value : null,
LicenseFile = license?.Attribute("type")?.Value == "file" ? license.Value : null,
LicenseId = license?.Attribute("type")?.Value is "expression" or "file" ? null : license?.Value,
ProjectUrl = Value("projectUrl"),
RepositoryUrl = repository?.Attribute("url")?.Value,
Description = Value("description"),
Copyright = Value("copyright")
};
}
public static string ReadPackageId(string nupkgPath) => ReadNuspecMetadata(nupkgPath).Id;
}

4
nukebuild/_build.csproj

@ -13,13 +13,13 @@
<ItemGroup>
<PackageReference Include="Nuke.Common" />
<PackageReference Include="Numerge" />
<!-- Keep in sync with Avalonia.Build.Tasks -->
<PackageReference Include="Mono.Cecil" />
<PackageReference Include="Microsoft.Build.Framework" PrivateAssets="All" />
<PackageReference Include="NuGet.Protocol" />
<PackageReference Include="System.Security.Cryptography.Xml" VersionOverride="10.0.11" /> <!-- Local override to avoid security warnings -->
<PackageDownload Include="Microsoft.DotNet.ApiCompat.Tool" Version="[10.0.100]" />
<PackageDownload Include="Microsoft.DotNet.ApiDiff.Tool" Version="[10.0.100-rtm.25531.102]" />
<PackageDownload Include="dotnet-ilrepack" Version="[2.0.44]" />
<PackageDownload Include="CycloneDX" Version="[6.2.0]" />
</ItemGroup>
<Import Project="..\build\MicroCOM.props" />

17
packages/Avalonia/AvaloniaBuildTasks.targets

@ -39,6 +39,23 @@
Condition="'$(ApplicationIcon)' != ''" />
</ItemGroup>
<Target Name="_InjectAvaloniaAdditionalFiles" BeforeTargets="GenerateMSBuildEditorConfigFileShouldRun">
<!-- Include AvaloniaXaml and AvaloniaResource (XAML only) as a Roslyn-visible additional files. -->
<!-- This is necessary to use these files in source generators and other Roslyn-based extension. -->
<!-- AdditionalFiles ensures that each file has Compilation available for them. -->
<ItemGroup>
<AdditionalFiles Include="@(AvaloniaXaml)" SourceItemGroup="AvaloniaXaml" />
<_AvaloniaResourceXamlOnly Include="@(AvaloniaResource)"
Condition="'%(Extension)' == '.xaml' OR '%(Extension)' == '.axaml'"
Exclude="@(AvaloniaXaml)" />
<AdditionalFiles Include="@(_AvaloniaResourceXamlOnly)" SourceItemGroup="AvaloniaResource" />
</ItemGroup>
<!-- Unfortunatelly, we can't include binary files as AdditionalFiles, so for general use avalonia resources we add AvaloniaResourcePaths instead -->
<PropertyGroup>
<AvaloniaResourcePaths>@(AvaloniaResource->'%(FullPath)', ',')</AvaloniaResourcePaths>
</PropertyGroup>
</Target>
<UsingTask TaskName="GenerateAvaloniaResourcesTask"
AssemblyFile="$(AvaloniaBuildTasksLocation)"
/>

2
readme.md

@ -10,7 +10,7 @@
Considered by many to be the spiritual successor to WPF, Avalonia UI provides a familiar, modern development experience for XAML developers creating cross-platform applications. While Avalonia UI is [similar to WPF](https://docs.avaloniaui.net/docs/next/get-started/wpf/), it isn't a 1:1 copy, and you'll find plenty of improvements.
For those seeking a cross-platform WPF, we have created [Avalonia XPF]([https://avaloniaui.net/xpf](https://avaloniaui.net/xpf?utm_source=github&utm_medium=referral&utm_content=readme_link)), enabling WPF applications to run on macOS and Linux with little to no code changes. Avalonia XPF is a commercial product and is licensed per-app, per-platform.
For those seeking a cross-platform WPF, we have created [Avalonia XPF](https://avaloniaui.net/xpf?utm_source=github&utm_medium=referral&utm_content=readme_link), enabling WPF applications to run on macOS and Linux with little to no code changes. Avalonia XPF is a commercial product and is licensed per-app, per-platform.
### Blog
To see the latest announcements and read about the state of Avalonia, check out the [Avalonia UI Blog](https://www.avaloniaui.net/blog?utm_source=github&utm_medium=referral&utm_content=readme_link).

2
samples/AppWithoutLifetime/Program.cs

@ -24,6 +24,8 @@ class Program
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
#if DEBUG
.WithDeveloperTools()
#endif
.LogToTrace();
}

3
samples/BindingDemo/App.xaml.cs

@ -24,7 +24,10 @@ namespace BindingDemo
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithDataAnnotationsValidation()
#if DEBUG
.WithDeveloperTools()
#endif
.LogToTrace();
}
}

10
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -14,16 +14,6 @@
</AndroidResource>
</ItemGroup>
<PropertyGroup Condition="'$(AndroidEnableProfiler)'=='True'">
<IsEmulator Condition="'$(IsEmulator)' == ''">True</IsEmulator>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<AndroidEnvironment Condition="'$(IsEmulator)'=='True'" Include="environment.emulator.txt" />
<AndroidEnvironment Condition="'$(IsEmulator)'!='True'" Include="environment.device.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" />
</ItemGroup>

1
samples/ControlCatalog.Android/environment.device.txt

@ -1 +0,0 @@
DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend

1
samples/ControlCatalog.Android/environment.emulator.txt

@ -1 +0,0 @@
DOTNET_DiagnosticPorts=10.0.2.2:9001,suspend

3
samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj

@ -21,13 +21,12 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Wayland\Avalonia.Wayland.csproj" />
<ProjectReference Include="..\..\src\Headless\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<!-- For native controls test -->
<PackageReference Include="MonoMac.NetStandard" />
</ItemGroup>
<PropertyGroup>

7
samples/ControlCatalog.Desktop/NativeControls/Gtk/EmbedSample.Gtk.cs

@ -21,10 +21,11 @@ public class EmbedSampleGtk : INativeDemoControl
}
var control = createDefault();
var nodes = Path.GetFullPath(Path.Combine(typeof(EmbedSample).Assembly.GetModules()[0].FullyQualifiedName,
"..", "NativeControls", "Gtk", "nodes.mp4"));
var nodesFile = Path.Combine(AppContext.BaseDirectory, "NativeControls", "Gtk", "nodes.mp4");
nodesFile = Path.GetFullPath(nodesFile);
_mplayer = Process.Start(new ProcessStartInfo("mplayer",
$"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodes}\"")
$"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodesFile}\"")
{
UseShellExecute = false,

54
samples/ControlCatalog.Desktop/NativeControls/Mac/EmbedSample.Mac.cs

@ -1,29 +1,57 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Threading;
using ControlCatalog.Pages;
using MonoMac.Foundation;
using MonoMac.WebKit;
namespace ControlCatalog.Desktop;
public class EmbedSampleMac : INativeDemoControl
{
private static bool? s_webKitLoaded;
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
// Note: We are using MonoMac for example purposes
// It shouldn't be used in production apps
MacHelper.EnsureInitialized();
s_webKitLoaded ??= NativeLibrary.TryLoad("/System/Library/Frameworks/WebKit.framework/WebKit", out _);
var webView = new WebView();
Dispatcher.UIThread.Post(() =>
if (s_webKitLoaded is false)
{
webView.MainFrame.LoadRequest(new NSUrlRequest(new NSUrl(
isSecond ? "https://bing.com" : "https://google.com/")));
});
return createDefault();
}
// alloc
var webView = ObjC.MsgSend(ObjC.GetClass("WKWebView"), ObjC.GetUid("alloc"));
// init
webView = ObjC.MsgSend(webView, ObjC.GetUid("init"));
// ns url
var urlNsString = ObjC.CreateString(isSecond ? "https://bing.com" : "https://google.com/");
var url = ObjC.MsgSend(
ObjC.GetClass("NSURL"), ObjC.GetUid("URLWithString:"),
urlNsString);
// ns url request
var request = ObjC.MsgSend(ObjC.GetClass("NSURLRequest"), ObjC.GetUid("requestWithURL:"), url);
// load request
ObjC.MsgSend(webView, ObjC.GetUid("loadRequest:"), request);
return new MacOSViewHandle(webView);
}
private sealed class MacOSViewHandle(IntPtr view) : INativeControlHostDestroyableControlHandle
{
public IntPtr Handle { get; private set; } = view;
public string HandleDescriptor => "NSView";
public void Destroy()
{
if (Handle != IntPtr.Zero)
{
ObjC.MsgSend(Handle, ObjC.GetUid("release"));
Handle = IntPtr.Zero;
}
}
}
}

38
samples/ControlCatalog.Desktop/NativeControls/Mac/MacHelper.cs

@ -1,38 +0,0 @@
using System;
using Avalonia.Controls.Platform;
using MonoMac.AppKit;
namespace ControlCatalog.Desktop;
internal class MacHelper
{
private static bool _isInitialized;
public static void EnsureInitialized()
{
if (_isInitialized)
return;
_isInitialized = true;
NSApplication.Init();
}
}
internal class MacOSViewHandle : INativeControlHostDestroyableControlHandle
{
private NSView? _view;
public MacOSViewHandle(NSView view)
{
_view = view;
}
public IntPtr Handle => _view?.Handle ?? IntPtr.Zero;
public string HandleDescriptor => "NSView";
public void Destroy()
{
_view?.Dispose();
_view = null;
}
}

28
samples/ControlCatalog.Desktop/NativeControls/Mac/ObjC.cs

@ -0,0 +1,28 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Platform.Interop;
namespace ControlCatalog.Desktop;
internal static partial class ObjC
{
private const string LibObjC = "/usr/lib/libobjc.dylib";
public static IntPtr CreateString(string value)
{
using var utf8 = new Utf8Buffer(value);
return MsgSend(GetClass("NSString"), GetUid("stringWithUTF8String:"), utf8);
}
[LibraryImport(LibObjC, EntryPoint = "objc_getClass", StringMarshalling = StringMarshalling.Utf8)]
internal static partial IntPtr GetClass(string name);
[LibraryImport(LibObjC, EntryPoint = "sel_getUid", StringMarshalling = StringMarshalling.Utf8)]
internal static partial IntPtr GetUid(string selector);
[LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
internal static partial IntPtr MsgSend(IntPtr receiver, IntPtr selector);
[LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
internal static partial IntPtr MsgSend(IntPtr receiver, IntPtr selector, IntPtr arg);
}

4
samples/ControlCatalog.Desktop/Program.cs

@ -142,6 +142,8 @@ namespace ControlCatalog.Desktop
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithDataAnnotationsValidation()
.UseWaylandWithFallback()
.With(new X11PlatformOptions
{
EnableMultiTouch = true,
@ -162,7 +164,9 @@ namespace ControlCatalog.Desktop
})
.UseSkia()
.WithInterFont()
#if DEBUG
.WithDeveloperTools()
#endif
.AfterSetup(builder =>
{
EmbedSample.Implementation = OperatingSystem.IsWindows() ? new EmbedSampleWin()

3
samples/ControlCatalog/App.xaml

@ -60,6 +60,9 @@
<Style Selector="Label.h3">
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="RefreshContainer">
<Setter Property="IsMouseEnabled" Value="true" />
</Style>
</Application.Styles>
<NativeDock.Menu>
<NativeMenu>

6
samples/ControlCatalog/ControlCatalog.csproj

@ -42,12 +42,6 @@
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
</ItemGroup>
<ItemGroup>
<AvaloniaXaml Update="NavHeaderItem.xaml">
<Generator>MSBuild:Compile</Generator>
</AvaloniaXaml>
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\SourceGenerators.props" />
<Import Project="..\..\build\SkiaSharp.props" />

24
samples/ControlCatalog/Converter/FlexDemoNumberToThicknessConverter.cs

@ -0,0 +1,24 @@
using System;
using System.Globalization;
using Avalonia;
using Avalonia.Data.Converters;
namespace ControlCatalog.Converter
{
internal sealed class FlexDemoNumberToThicknessConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is int x && targetType.IsAssignableFrom(typeof(Thickness)))
{
var y = 16 + 2 * ((x * 5) % 9);
return new Thickness(2 * y, y);
}
throw new NotSupportedException();
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
}

31
samples/ControlCatalog/Converter/GdpToBrushConverter.cs

@ -0,0 +1,31 @@
using System;
using System.Globalization;
using Avalonia;
using Avalonia.Data.Converters;
using Avalonia.Media;
using Avalonia.Media.Immutable;
namespace ControlCatalog.Converter;
public sealed class GdpToBrushConverter : IValueConverter
{
private readonly ImmutableSolidColorBrush _orangeBrush = new(Colors.Orange, 0.6);
private readonly ImmutableSolidColorBrush _yellowBrush = new(Colors.Yellow, 0.6);
private readonly ImmutableSolidColorBrush _greenBrush = new(Colors.LightGreen, 0.6);
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not int gdp)
return AvaloniaProperty.UnsetValue;
return gdp switch
{
<= 5000 => _orangeBrush,
<= 10000 => _yellowBrush,
_ => _greenBrush
};
}
object IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}

27
samples/ControlCatalog/Converter/TableViewColumnWidthConverter.cs

@ -0,0 +1,27 @@
using System;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data.Converters;
namespace ControlCatalog.Converter;
public sealed class TableViewColumnWidthConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool useStarSize &&
parameter is string stringParameter &&
double.TryParse(stringParameter, NumberStyles.Number, CultureInfo.InvariantCulture, out var baseWidth))
{
return useStarSize ?
new GridLength(baseWidth, GridUnitType.Star) :
new GridLength(baseWidth * 100, GridUnitType.Pixel);
}
return AvaloniaProperty.UnsetValue;
}
object? IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}

2
samples/ControlCatalog/MainView.xaml

@ -159,4 +159,4 @@
</DrawerPage.DrawerFooterTemplate>
<NavigationPage Name="NavPage"/>
</DrawerPage>
</DrawerPage>

2
samples/ControlCatalog/MainWindow.xaml

@ -12,6 +12,8 @@
CanResize="{Binding CanResize}"
CanMinimize="{Binding CanMinimize}"
CanMaximize="{Binding CanMaximize}"
WindowDrawnDecorations.TitleBarDecorations="{Binding TitleBarDecorations}"
Win32Properties.WindowCornerPreference="{Binding Win32WindowCornerPreference}"
x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"
x:DataType="vm:MainWindowViewModel">
<NativeMenu.Menu>

4
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs

@ -102,8 +102,8 @@ namespace ControlCatalog.Pages
return String.Format("{0} ({1})", parts.ToArray());
});
var binding = new MultiBinding { Converter = converter };
binding.Bindings.Add(new Binding("Name"));
binding.Bindings.Add(new Binding("Abbreviation"));
binding.Bindings.Add(CompiledBinding.Create<StateData, string>(s => s.Name));
binding.Bindings.Add(CompiledBinding.Create<StateData, string>(s => s.Abbreviation));
MultiBindingBox.ValueMemberBinding = binding;

3
samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml

@ -49,6 +49,9 @@
<TextBlock Text="Validation Example"/>
<CalendarDatePicker SelectedDate="{CompiledBinding ValidatedDateExample, Mode=TwoWay}"/>
<TextBlock Text="WeekNumbers example"/>
<CalendarDatePicker IsWeekNumberVisible="True" />
</StackPanel>
</StackPanel>

11
samples/ControlCatalog/Pages/CalendarPage.xaml

@ -48,6 +48,17 @@
<Calendar Name="BlackoutDatesCalendar"
SelectionMode="SingleDate" />
</StackPanel>
<StackPanel>
<TextBlock Text="WeekNumbers (ISO-Week)" />
<Calendar Name="WeekNumbers"
WeekNumberRule="FirstFourDayWeek"
FirstDayOfWeek="Monday"
IsWeekNumberVisible="True">
<Calendar.Resources>
<x:String x:Key="StringCalendarWeekNumberHeader">#</x:String>
</Calendar.Resources>
</Calendar>
</StackPanel>
</WrapPanel>
</StackPanel>
</ContentPage>

3
samples/ControlCatalog/Pages/CarouselPage/CareCompanionAppPage.xaml.cs

@ -5,6 +5,7 @@ using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
@ -130,7 +131,7 @@ public partial class CareCompanionAppPage : UserControl
};
pager.Bind(PipsPager.SelectedPageIndexProperty,
new Avalonia.Data.Binding("SelectedIndex") { Source = carousel, Mode = Avalonia.Data.BindingMode.TwoWay });
CompiledBinding.Create<AvaCarouselPage, int>(c => c.SelectedIndex, carousel, mode: BindingMode.TwoWay));
return pager;
}

263
samples/ControlCatalog/Pages/CompositionPage.axaml

@ -1,61 +1,204 @@
<TabbedPage xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pages="using:ControlCatalog.Pages"
Header="Composition"
x:Class="ControlCatalog.Pages.CompositionPage">
<ContentPage Theme="{StaticResource ScrollPage}" Header="Implicit animations">
<StackPanel>
<Grid ColumnDefinitions="*,10,40" Margin="0 0 40 0">
<ItemsControl x:Name="Items">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.DataTemplates>
<DataTemplate DataType="pages:CompositionPageColorItem">
<Border
pages:CompositionPage.EnableAnimations="True"
Padding="10" BorderBrush="Gray" BorderThickness="2"
Background="{Binding ColorBrush}" Width="100" Height="100" Margin="10">
<TextBlock Text="{Binding ColorHexValue}"/>
</Border>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
<GridSplitter Margin="2" BorderThickness="1" BorderBrush="Gray"
Background="#e0e0e0" Grid.Column="1"
ResizeDirection="Columns" ResizeBehavior="PreviousAndNext"
/>
<Border Grid.Column="2">
<LayoutTransformControl HorizontalAlignment="Center" MinWidth="30">
<LayoutTransformControl.LayoutTransform>
<RotateTransform Angle="90"/>
</LayoutTransformControl.LayoutTransform>
<TextBlock>Resize me</TextBlock>
</LayoutTransformControl>
</Border>
</Grid>
</StackPanel>
</ContentPage>
<ContentPage Theme="{StaticResource ScrollPage}" Header="Animation">
<DockPanel>
<Button DockPanel.Dock="Top" Margin="10" Click="ButtonThreadSleep">Thread.Sleep(10000);</Button>
<Control x:Name="SolidVisualHost" />
</DockPanel>
</ContentPage>
<ContentPage Theme="{StaticResource ScrollPage}" Header="Custom">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Button Margin="10" Click="ButtonThreadSleep">Thread.Sleep(10000);</Button>
<Button Margin="10" Click="ButtonStartCustomVisual">Start</Button>
<Button Margin="10" Click="ButtonStopCustomVisual">Stop</Button>
<CheckBox Margin="10"
x:Name="PreciseDirtyRectsCheckboxCustomVisual"
IsCheckedChanged="PreciseDirtyRectsCheckboxCustomVisualChanged"
>Precise dirty rects</CheckBox>
</StackPanel>
<Control x:Name="CustomVisualHost" />
</DockPanel>
</ContentPage>
<TabbedPage
x:Class="ControlCatalog.Pages.CompositionPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pages="using:ControlCatalog.Pages">
<ContentPage Header="Implicit">
<StackPanel>
<Grid Margin="0,0,40,0" ColumnDefinitions="*,10,40">
<ItemsControl x:Name="Items">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.DataTemplates>
<DataTemplate DataType="pages:CompositionPageColorItem">
<Border
Width="100"
Height="100"
Margin="10"
Padding="10"
pages:CompositionPage.EnableAnimations="True"
Background="{Binding ColorBrush}"
BorderBrush="Gray"
BorderThickness="2">
<TextBlock Text="{Binding ColorHexValue}" />
</Border>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
<GridSplitter
Grid.Column="1"
Margin="2"
Background="#e0e0e0"
BorderBrush="Gray"
BorderThickness="1"
ResizeBehavior="PreviousAndNext"
ResizeDirection="Columns" />
<Border Grid.Column="2">
<LayoutTransformControl MinWidth="30" HorizontalAlignment="Center">
<LayoutTransformControl.LayoutTransform>
<RotateTransform Angle="90" />
</LayoutTransformControl.LayoutTransform>
<TextBlock>Resize me</TextBlock>
</LayoutTransformControl>
</Border>
</Grid>
</StackPanel>
</ContentPage>
<ContentPage Header="Animations">
<DockPanel>
<Button
Margin="10"
Click="ButtonThreadSleep"
DockPanel.Dock="Top">
Thread.Sleep(10000);
</Button>
<Control x:Name="SolidVisualHost" />
</DockPanel>
</ContentPage>
<ContentPage Header="Custom">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Button Margin="10" Click="ButtonThreadSleep">Thread.Sleep(5000);</Button>
<Button Margin="10" Click="ButtonStartCustomVisual">Start</Button>
<Button Margin="10" Click="ButtonStopCustomVisual">Stop</Button>
<CheckBox
x:Name="PreciseDirtyRectsCheckboxCustomVisual"
Margin="10"
IsCheckedChanged="PreciseDirtyRectsCheckboxCustomVisualChanged">
Precise dirty rects
</CheckBox>
</StackPanel>
<Control x:Name="CustomVisualHost" />
</DockPanel>
</ContentPage>
<ContentPage Header="Brushes">
<ScrollViewer>
<StackPanel>
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="BorderThickness" Value="2" />
</Style>
</StackPanel.Styles>
<Button Margin="10" Click="ButtonThreadSleep">
Thread.Sleep(5000);
</Button>
<UniformGrid
Margin="12"
ColumnSpacing="12"
RowSpacing="12">
<!-- Solid Color -->
<StackPanel Spacing="6">
<TextBlock
FontWeight="Bold"
Foreground="{Binding #SolidBrushHost.Background, Mode=OneWay}"
Text="SolidColorBrush" />
<Border
x:Name="SolidBrushHost"
Height="120"
Background="Gray"
BorderBrush="DarkGray"
BorderThickness="1"
CornerRadius="8">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18"
Text="Solid" />
</Border>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
BorderBrush="{Binding #SolidBrushHost.Background, Mode=OneWay}"
Click="SolidBrushCreateAnimate_Click"
Content="Create" />
</StackPanel>
</StackPanel>
<!-- Linear Gradient -->
<StackPanel Spacing="6">
<TextBlock
FontWeight="Bold"
Foreground="{Binding #LinearBrushHost.Background, Mode=OneWay}"
Text="LinearGradientBrush" />
<Border
x:Name="LinearBrushHost"
Height="120"
Background="Gray"
BorderBrush="DarkGray"
BorderThickness="1"
CornerRadius="8">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18"
Text="Linear" />
</Border>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
BorderBrush="{Binding #LinearBrushHost.Background, Mode=OneWay}"
Click="LinearBrushCreateAnimate_Click"
Content="Create" />
</StackPanel>
</StackPanel>
<!-- Radial Gradient -->
<StackPanel Spacing="6">
<TextBlock
FontWeight="Bold"
Foreground="{Binding #RadialBrushHost.Background, Mode=OneWay}"
Text="RadialGradientBrush" />
<Border
x:Name="RadialBrushHost"
Height="120"
Background="Gray"
BorderBrush="DarkGray"
BorderThickness="1"
CornerRadius="8">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18"
Text="Radial" />
</Border>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
BorderBrush="{Binding #RadialBrushHost.Background, Mode=OneWay}"
Click="RadialBrushCreateAnimate_Click"
Content="Create" />
</StackPanel>
</StackPanel>
<!-- Conic Gradient -->
<StackPanel Spacing="6">
<TextBlock
FontWeight="Bold"
Foreground="{Binding #ConicBrushHost.Background, Mode=OneWay}"
Text="ConicGradientBrush" />
<Border
x:Name="ConicBrushHost"
Height="120"
Background="Gray"
BorderBrush="DarkGray"
BorderThickness="1"
CornerRadius="8">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18"
Text="Conic" />
</Border>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
BorderBrush="{Binding #ConicBrushHost.Background, Mode=OneWay}"
Click="ConicBrushCreateAnimate_Click"
Content="Create" />
</StackPanel>
</StackPanel>
</UniformGrid>
</StackPanel>
</ScrollViewer>
</ContentPage>
</TabbedPage>

179
samples/ControlCatalog/Pages/CompositionPage.axaml.cs

@ -315,7 +315,7 @@ public partial class CompositionPage : TabbedPage
private void ButtonThreadSleep(object? sender, RoutedEventArgs e)
{
Thread.Sleep(10000);
Thread.Sleep(5000);
}
private void ButtonStartCustomVisual(object? sender, RoutedEventArgs e)
@ -334,6 +334,183 @@ public partial class CompositionPage : TabbedPage
? CustomVisualHandler.UsePreciseDirtyRects
: CustomVisualHandler.UseNonPreciseDirtyRects);
}
private void SolidBrushCreateAnimate_Click(object? sender, RoutedEventArgs e)
{
var visual = ElementComposition.GetElementVisual(SolidBrushHost);
if (visual == null)
return;
var compositor = visual.Compositor;
var brush = SolidBrushHost.Background as CompositionSolidColorBrush
?? compositor.CreateSolidColorBrush();
brush.Color = Color.FromRgb(168, 213, 85);
SolidBrushHost.Background = brush;
var animation = compositor.CreateColorKeyFrameAnimation();
animation.InsertKeyFrame(0f, Color.FromRgb(168, 213, 85));
animation.InsertKeyFrame(0.25f, Color.FromRgb(31, 167, 168));
animation.InsertKeyFrame(0.75f, Color.FromRgb(44, 121, 251));
animation.InsertKeyFrame(1f, Color.FromRgb(168, 213, 85));
animation.Duration = TimeSpan.FromSeconds(4);
animation.IterationBehavior = AnimationIterationBehavior.Forever;
brush.StartAnimation("Color", animation);
}
private void LinearBrushCreateAnimate_Click(object? sender, RoutedEventArgs e)
{
var visual = ElementComposition.GetElementVisual(LinearBrushHost);
if (visual == null)
return;
var compositor = visual.Compositor;
var brush = LinearBrushHost.Background as CompositionLinearGradientBrush
?? compositor.CreateLinearGradientBrush();
brush.GradientStops.Clear();
brush.GradientStops.Add(compositor.CreateGradientStop(0f, Color.FromRgb(168, 213, 85)));
brush.GradientStops.Add(compositor.CreateGradientStop(0.33f, Color.FromRgb(31, 167, 168)));
brush.GradientStops.Add(compositor.CreateGradientStop(0.66f, Color.FromRgb(44, 121, 251)));
brush.GradientStops.Add(compositor.CreateGradientStop(1f, Color.FromRgb(168, 213, 85)));
brush.StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative);
brush.EndPoint = new RelativePoint(1, 0, RelativeUnit.Relative);
LinearBrushHost.Background = brush;
var start = compositor.CreateRelativePointKeyFrameAnimation();
start.Duration = TimeSpan.FromSeconds(4);
start.IterationBehavior = AnimationIterationBehavior.Forever;
start.InsertKeyFrame(0f, new RelativePoint(-1, 0, RelativeUnit.Relative));
start.InsertKeyFrame(0.5f, new RelativePoint(1, 0, RelativeUnit.Relative));
start.InsertKeyFrame(1f, new RelativePoint(-1, 0, RelativeUnit.Relative));
var end = compositor.CreateRelativePointKeyFrameAnimation();
end.Duration = start.Duration;
end.IterationBehavior = AnimationIterationBehavior.Forever;
end.InsertKeyFrame(0f, new RelativePoint(0, 0, RelativeUnit.Relative));
end.InsertKeyFrame(0.5f, new RelativePoint(2, 0, RelativeUnit.Relative));
end.InsertKeyFrame(1f, new RelativePoint(0, 0, RelativeUnit.Relative));
brush.StartAnimation("StartPoint", start);
brush.StartAnimation("EndPoint", end);
AnimateGradientStops(brush);
}
private void RadialBrushCreateAnimate_Click(object? sender, RoutedEventArgs e)
{
var visual = ElementComposition.GetElementVisual(RadialBrushHost);
if (visual == null)
return;
var compositor = visual.Compositor;
var brush = RadialBrushHost.Background as CompositionRadialGradientBrush
?? compositor.CreateRadialGradientBrush();
brush.GradientStops.Clear();
brush.GradientStops.Add(compositor.CreateGradientStop(0f, Colors.Yellow));
brush.GradientStops.Add(compositor.CreateGradientStop(0.6f, Colors.Orange));
brush.GradientStops.Add(compositor.CreateGradientStop(1f, Colors.Red));
brush.Center = RelativePoint.Center;
brush.GradientOrigin = RelativePoint.Center;
RadialBrushHost.Background = brush;
var centerAnim = compositor.CreateRelativePointKeyFrameAnimation();
centerAnim.Duration = TimeSpan.FromSeconds(3);
centerAnim.IterationBehavior = AnimationIterationBehavior.Forever;
centerAnim.InsertKeyFrame(0f, new RelativePoint(0.2, 0.5, RelativeUnit.Relative));
centerAnim.InsertKeyFrame(0.5f, new RelativePoint(0.8, 0.5, RelativeUnit.Relative));
centerAnim.InsertKeyFrame(1f, new RelativePoint(0.2, 0.5, RelativeUnit.Relative));
brush.StartAnimation("Center", centerAnim);
var radiusXExpression = compositor.CreateExpressionAnimation(
"RelativeScalar(Clamp(0.2 + Abs(this.Target.Center.X - 0.5) * 0.9, 0.2, 0.85), RelativeUnit.Relative)");
var radiusYExpression = compositor.CreateExpressionAnimation(
"RelativeScalar(Clamp(0.2 + Abs(this.Target.Center.Y - 0.5) * 0.9, 0.2, 0.85), RelativeUnit.Relative)");
brush.StartAnimation("RadiusX", radiusXExpression);
brush.StartAnimation("RadiusY", radiusYExpression);
AnimateGradientStops(brush);
}
private void ConicBrushCreateAnimate_Click(object? sender, RoutedEventArgs e)
{
var visual = ElementComposition.GetElementVisual(ConicBrushHost);
if (visual == null)
return;
var compositor = visual.Compositor;
var brush = ConicBrushHost.Background as CompositionConicGradientBrush
?? compositor.CreateConicGradientBrush();
brush.GradientStops.Clear();
brush.GradientStops.Add(compositor.CreateGradientStop(0f, Colors.Cyan));
brush.GradientStops.Add(compositor.CreateGradientStop(0.25f, Colors.Magenta));
brush.GradientStops.Add(compositor.CreateGradientStop(0.5f, Colors.Yellow));
brush.GradientStops.Add(compositor.CreateGradientStop(0.75f, Colors.Lime));
brush.GradientStops.Add(compositor.CreateGradientStop(1f, Colors.Cyan));
brush.Center = RelativePoint.Center;
ConicBrushHost.Background = brush;
var angleAnim = compositor.CreateScalarKeyFrameAnimation();
angleAnim.Duration = TimeSpan.FromSeconds(5);
angleAnim.IterationBehavior = AnimationIterationBehavior.Forever;
angleAnim.InsertKeyFrame(0f, 0f);
angleAnim.InsertKeyFrame(1f, 360f);
brush.StartAnimation("Angle", angleAnim);
var centerExpression = compositor.CreateExpressionAnimation(
"RelativePoint(Abs(Cos(ToRadians(this.Target.Angle))), Abs(Sin(ToRadians(this.Target.Angle))), RelativeUnit.Relative)");
brush.StartAnimation("Center", centerExpression);
AnimateGradientStops(brush, false);
}
private void AnimateGradientStops(CompositionGradientBrush? brush, bool animateOffsets = true)
{
if (brush == null)
return;
var compositor = brush.Compositor;
foreach (var stop in brush.GradientStops)
{
if (stop is not CompositionGradientStop gs)
continue;
var colorAnim = compositor.CreateColorKeyFrameAnimation();
colorAnim.Duration = TimeSpan.FromSeconds(4);
colorAnim.IterationBehavior = AnimationIterationBehavior.Forever;
colorAnim.InsertKeyFrame(0f, gs.Color);
colorAnim.InsertKeyFrame(0.5f, ShiftColor(gs.Color));
colorAnim.InsertKeyFrame(1f, gs.Color);
gs.StartAnimation("Color", colorAnim);
if (!animateOffsets)
continue;
var offsetAnim = compositor.CreateScalarKeyFrameAnimation();
offsetAnim.Duration = TimeSpan.FromSeconds(4);
offsetAnim.IterationBehavior = AnimationIterationBehavior.Forever;
var o = (float)gs.Offset;
var o2 = Math.Clamp(gs.Offset + 0.1, 0, 1);
offsetAnim.InsertKeyFrame(0f, o);
offsetAnim.InsertKeyFrame(0.5f, (float)o2);
offsetAnim.InsertKeyFrame(1f, o);
gs.StartAnimation("Offset", offsetAnim);
}
}
private static Color ShiftColor(Color c)
{
var r = (byte)Math.Min(255, (int)(c.R * 1.15) + 30);
var g = (byte)Math.Max(0, (int)(c.G * 0.75) - 10);
var b = (byte)Math.Min(255, (int)(c.B * 1.2) + 20);
return Color.FromArgb(c.A, r, g, b);
}
}
public class CompositionPageColorItem

12
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -1,12 +0,0 @@
<ContentPage Theme="{StaticResource ScrollPage}" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Header="DataGrid"
x:Class="ControlCatalog.Pages.DataGridPage">
<StackPanel>
<TextBlock Text="DataGrid has moved to its own repository!" Margin="8" />
<HyperlinkButton Content="https://github.com/AvaloniaUI/Avalonia.Controls.DataGrid"
Click="OnLinkClicked" />
</StackPanel>
</ContentPage>

22
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@ -1,22 +0,0 @@
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages;
public partial class DataGridPage : ContentPage
{
public DataGridPage()
{
InitializeComponent();
}
private void OnLinkClicked(object? sender, RoutedEventArgs e)
{
Process.Start(new ProcessStartInfo
{
FileName = "https://github.com/AvaloniaUI/Avalonia.Controls.DataGrid",
UseShellExecute = true
});
}
}

4
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@ -44,14 +44,14 @@
MaxWidth="260"
Background="{DynamicResource SystemAccentColorDark1}"
DragDrop.AllowDrop="True">
<TextBlock TextWrapping="Wrap">Drop some text or files here (Copy)</TextBlock>
<TextBlock TextWrapping="Wrap">Drop some text, files, bitmap or custom format here (Copy)</TextBlock>
</Border>
<Border Name="MoveTarget"
Padding="16"
MaxWidth="260"
Background="{DynamicResource SystemAccentColorDark1}"
DragDrop.AllowDrop="True">
<TextBlock TextWrapping="Wrap">Drop some text or files here (Move)</TextBlock>
<TextBlock TextWrapping="Wrap">Drop some text or custom format here (Move)</TextBlock>
</Border>
</StackPanel>
</WrapPanel>

45
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
@ -38,9 +39,11 @@ namespace ControlCatalog.Pages
"Files",
async d =>
{
if (Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName is { } name &&
var currentFile = Environment.ProcessPath;
if (File.Exists(currentFile) &&
TopLevel.GetTopLevel(this) is { } topLevel &&
await topLevel.StorageProvider.TryGetFileFromPathAsync(name) is { } storageFile)
await topLevel.StorageProvider.TryGetFileFromPathAsync(currentFile) is { } storageFile)
{
d.Add(DataTransferItem.Create(DataFormat.File, storageFile));
}
@ -51,6 +54,9 @@ namespace ControlCatalog.Pages
"Bitmap",
d => d.Add(DataTransferItem.Create(DataFormat.Bitmap, new Bitmap(AssetLoader.Open(new Uri("avares://ControlCatalog/Assets/image1.jpg"))))),
DragDropEffects.Copy);
AddHandlers(CopyTarget, DragDropEffects.Copy);
AddHandlers(MoveTarget, DragDropEffects.Move);
}
private void SetupDnd(string suffix, Action<DataTransfer> factory, DragDropEffects effects) =>
@ -94,16 +100,18 @@ namespace ControlCatalog.Pages
}
}
dragMe.PointerPressed += DoDrag;
}
private void AddHandlers(Control target, DragDropEffects allowedEffects)
{
DragDrop.AddDragEnterHandler(target, DragOver);
DragDrop.AddDragOverHandler(target, DragOver);
DragDrop.AddDropHandler(target, Drop);
void DragOver(object? sender, DragEventArgs e)
{
if (e.Source is Control c && c.Name == "MoveTarget")
{
e.DragEffects = e.DragEffects & (DragDropEffects.Move);
}
else
{
e.DragEffects = e.DragEffects & (DragDropEffects.Copy);
}
e.DragEffects &= allowedEffects;
// Only allow if the dragged data contains text or filenames.
if (!e.DataTransfer.Contains(DataFormat.Text)
@ -115,14 +123,10 @@ namespace ControlCatalog.Pages
async void Drop(object? sender, DragEventArgs e)
{
if (e.Source is Control c && c.Name == "MoveTarget")
{
e.DragEffects = e.DragEffects & (DragDropEffects.Move);
}
else
{
e.DragEffects = e.DragEffects & (DragDropEffects.Copy);
}
e.DragEffects &= allowedEffects;
if (e.DragEffects == DragDropEffects.None)
return;
if (e.DataTransfer.Contains(DataFormat.Text))
{
@ -165,11 +169,6 @@ namespace ControlCatalog.Pages
DropState.Content = "Custom: " + e.DataTransfer.TryGetValue(_customFormat);
}
}
dragMe.PointerPressed += DoDrag;
AddHandler(DragDrop.DropEvent, Drop);
AddHandler(DragDrop.DragOverEvent, DragOver);
}
}
}

219
samples/ControlCatalog/Pages/FlexPage.axaml

@ -0,0 +1,219 @@
<ContentPage xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:panels="using:Avalonia.Labs.Panels"
xmlns:conv="using:ControlCatalog.Converter"
xmlns:vm="using:ControlCatalog.ViewModels"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:DataType="vm:FlexViewModel"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
x:Class="ControlCatalog.Pages.FlexPage">
<ContentPage.Resources>
<conv:FlexDemoNumberToThicknessConverter x:Key="NumberToThicknessConverter" />
<DataTemplate x:Key="ItemTemplate"
x:DataType="vm:FlexItemViewModel">
<ListBoxItem Padding="{Binding Value, Converter={StaticResource NumberToThicknessConverter}}"
IsSelected="{Binding IsSelected}"
Background="{Binding Color}"
Tapped="OnItemTapped">
<ListBoxItem.Styles>
<Style Selector="ListBoxItem:selected">
<Setter Property="Background" Value="DodgerBlue" />
</Style>
</ListBoxItem.Styles>
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Value}" />
</ListBoxItem>
</DataTemplate>
</ContentPage.Resources>
<ContentPage.Styles>
<Style Selector="CheckBox">
<Setter Property="MinWidth" Value="0" />
<Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="ComboBox">
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
<Style Selector="RadioButton">
<Setter Property="MinWidth" Value="0" />
<Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
</ContentPage.Styles>
<DockPanel Margin="16">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
Padding="0,0,16,0"
MinWidth="200">
<StackPanel Spacing="16">
<TextBlock FontSize="16"
Text="Properties" />
<!--<panels:FlexPanel ColumnSpacing="16">
<RadioButton IsChecked="{Binding IsItemsControl}"
Content="ItemsControl" />
<RadioButton IsChecked="{Binding IsItemsRepeater}"
Content="ItemsRepeater" />
</panels:FlexPanel>-->
<StackPanel Spacing="8">
<TextBlock Text="Direction:" />
<ComboBox ItemsSource="{Binding DirectionValues}"
SelectedItem="{Binding Direction}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="JustifyContent:" />
<ComboBox ItemsSource="{Binding JustifyContentValues}"
SelectedItem="{Binding JustifyContent}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="AlignItems:" />
<ComboBox ItemsSource="{Binding AlignItemsValues}"
SelectedItem="{Binding AlignItems}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="AlignContent:" />
<ComboBox ItemsSource="{Binding AlignContentValues}"
SelectedItem="{Binding AlignContent}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="Wrap:" />
<ComboBox ItemsSource="{Binding WrapValues}"
SelectedItem="{Binding Wrap}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="ColumnSpacing:" />
<NumericUpDown Minimum="0"
Value="{Binding ColumnSpacing}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="RowSpacing:" />
<NumericUpDown Minimum="0"
Value="{Binding RowSpacing}" />
</StackPanel>
<CheckBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
Content="SelectedItem IsVisible"
IsChecked="{Binding SelectedItem.IsVisible}" />
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem AlignSelf:" />
<ComboBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
ItemsSource="{Binding AlignSelfValues}"
SelectedItem="{Binding SelectedItem.AlignSelfItem}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem Order:" />
<NumericUpDown IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
Value="{Binding SelectedItem.Order}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem Shrink:" />
<NumericUpDown Minimum="0.0"
IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
Value="{Binding SelectedItem.Shrink}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem Grow:" />
<NumericUpDown Minimum="0.0"
IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
Value="{Binding SelectedItem.Grow}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem Basis:" />
<ComboBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
ItemsSource="{Binding FlexBasisKindValues}"
SelectedItem="{Binding SelectedItem.BasisKind}" />
<NumericUpDown Minimum="0.0"
IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
Value="{Binding SelectedItem.BasisValue}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem HorizontalAlignment:" />
<ComboBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
ItemsSource="{Binding HorizontalAlignmentValues}"
SelectedItem="{Binding SelectedItem.HorizontalAlignment}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem VerticalAlignment:" />
<ComboBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
ItemsSource="{Binding VerticalAlignmentValues}"
SelectedItem="{Binding SelectedItem.VerticalAlignment}" />
</StackPanel>
<Grid ColumnDefinitions="*,8,*">
<Button Grid.Column="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Command="{Binding AddItemCommand}"
Content="Add Item" />
<Button Grid.Column="2"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Command="{Binding RemoveItemCommand}"
Content="Remove Item" />
</Grid>
</StackPanel>
</ScrollViewer>
<ItemsControl BorderBrush="#777"
BorderThickness="1"
ItemsSource="{Binding Numbers}"
ItemTemplate="{StaticResource ItemTemplate}">
<ItemsControl.Styles>
<Style Selector="ContentPresenter"
x:DataType="vm:FlexItemViewModel">
<Setter Property="Flex.AlignSelf" Value="{Binding AlignSelf}" />
<Setter Property="Flex.Order" Value="{Binding Order}" />
<Setter Property="Flex.Shrink" Value="{Binding Shrink}" />
<Setter Property="Flex.Grow" Value="{Binding Grow}" />
<Setter Property="Flex.Basis" Value="{Binding Basis}" />
<Setter Property="HorizontalAlignment" Value="{Binding HorizontalAlignment}" />
<Setter Property="VerticalAlignment" Value="{Binding VerticalAlignment}" />
<Setter Property="IsVisible" Value="{Binding IsVisible}" />
</Style>
</ItemsControl.Styles>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<FlexPanel Direction="{Binding Direction}"
JustifyContent="{Binding JustifyContent}"
AlignItems="{Binding AlignItems}"
AlignContent="{Binding AlignContent}"
Wrap="{Binding Wrap}"
ColumnSpacing="{Binding ColumnSpacing}"
RowSpacing="{Binding RowSpacing}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DockPanel>
</ContentPage>

37
samples/ControlCatalog/Pages/FlexPage.axaml.cs

@ -0,0 +1,37 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public partial class FlexPage : ContentPage
{
public FlexPage()
{
InitializeComponent();
DataContext = new FlexViewModel();
}
private void OnItemTapped(object? sender, RoutedEventArgs e)
{
if (sender is ListBoxItem control && DataContext is FlexViewModel vm && control.DataContext is FlexItemViewModel item)
{
if (vm.SelectedItem != null)
{
vm.SelectedItem.IsSelected = false;
}
if (vm.SelectedItem == item)
{
vm.SelectedItem = null;
}
else
{
vm.SelectedItem = item;
vm.SelectedItem.IsSelected = true;
}
}
}
}
}

6
samples/ControlCatalog/Pages/NotificationsPage.xaml

@ -17,11 +17,7 @@
<TextBlock DockPanel.Dock="Top"
Margin="2" Classes="h2" TextWrapping="Wrap">XAML only notification manager.</TextBlock>
<Button DockPanel.Dock="Top"
Content="Show XAML only Notification" Command="{Binding #ControlNotifications.Show}">
<Button.CommandParameter>
<Notification Title="Title" Message="Message" OnClick="NotificationOnClick" />
</Button.CommandParameter>
</Button>
Content="Show XAML only Notification" Click="ShowNotification" />
<Border Padding="10" BorderBrush="{DynamicResource SystemAccentColor}">
<WindowNotificationManager x:Name="ControlNotifications" />
</Border>

10
samples/ControlCatalog/Pages/NotificationsPage.xaml.cs

@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Interactivity;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
@ -25,9 +26,14 @@ namespace ControlCatalog.Pages
_viewModel.NotificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
}
public void NotificationOnClick()
private void ShowNotification(object? sender, RoutedEventArgs e)
{
ControlNotifications.Show("Notification clicked");
ControlNotifications.Show(new Notification
{
OnClick = () => ControlNotifications.Show("Notification clicked"),
Title = "Title",
Message = "Message"
});
}
}
}

2
samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs

@ -71,7 +71,7 @@ namespace ControlCatalog.Pages
if (_spinnerLocations == null)
{
_spinnerLocations = new List<Location>();
foreach (Location value in Enum.GetValues(typeof(Location)))
foreach (var value in Enum.GetValues<Location>())
{
_spinnerLocations.Add(value);
}

136
samples/ControlCatalog/Pages/PlatformSettingsPage.xaml

@ -0,0 +1,136 @@
<ContentPage Theme="{StaticResource ScrollPage}" x:Class="ControlCatalog.Pages.PlatformSettingsPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="using:ControlCatalog.ViewModels"
d:DesignHeight="800"
d:DesignWidth="600"
mc:Ignorable="d"
Header="Platform Settings"
x:DataType="viewModels:PlatformSettingsViewModel">
<StackPanel Spacing="20">
<TextBlock Text="This page displays IPlatformSettings values which may change at runtime based on system settings."
TextWrapping="Wrap"
Opacity="0.7" />
<GroupBox Header="System Color Values">
<StackPanel Spacing="15">
<Grid ColumnDefinitions="180,*" RowDefinitions="Auto,Auto">
<TextBlock Grid.Column="0" Grid.Row="0" Text="Theme Variant:" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding ThemeVariant}" FontWeight="Bold" VerticalAlignment="Center" />
<TextBlock Grid.Column="0" Grid.Row="1" Text="Contrast Preference:" VerticalAlignment="Center" Margin="0,10,0,0" />
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding ContrastPreference}" FontWeight="Bold" VerticalAlignment="Center" Margin="0,10,0,0" />
</Grid>
<StackPanel Spacing="10">
<Grid ColumnDefinitions="180,120,*">
<TextBlock Grid.Column="0" Text="Accent Color 1:" VerticalAlignment="Center" />
<Border Grid.Column="1"
Width="100"
Height="40"
BorderBrush="Black"
BorderThickness="1"
CornerRadius="4"
HorizontalAlignment="Left">
<Border.Background>
<SolidColorBrush Color="{Binding AccentColor1}" />
</Border.Background>
</Border>
<TextBlock Grid.Column="2"
Text="{Binding AccentColor1}"
VerticalAlignment="Center"
Margin="10,0,0,0" />
</Grid>
<Grid ColumnDefinitions="180,120,*">
<TextBlock Grid.Column="0" Text="Accent Color 2:" VerticalAlignment="Center" />
<Border Grid.Column="1"
Width="100"
Height="40"
BorderBrush="Black"
BorderThickness="1"
CornerRadius="4"
HorizontalAlignment="Left">
<Border.Background>
<SolidColorBrush Color="{Binding AccentColor2}" />
</Border.Background>
</Border>
<TextBlock Grid.Column="2"
Text="{Binding AccentColor2}"
VerticalAlignment="Center"
Margin="10,0,0,0" />
</Grid>
<Grid ColumnDefinitions="180,120,*">
<TextBlock Grid.Column="0" Text="Accent Color 3:" VerticalAlignment="Center" />
<Border Grid.Column="1"
Width="100"
Height="40"
BorderBrush="Black"
BorderThickness="1"
CornerRadius="4"
HorizontalAlignment="Left">
<Border.Background>
<SolidColorBrush Color="{Binding AccentColor3}" />
</Border.Background>
</Border>
<TextBlock Grid.Column="2"
Text="{Binding AccentColor3}"
VerticalAlignment="Center"
Margin="10,0,0,0" />
</Grid>
</StackPanel>
</StackPanel>
</GroupBox>
<GroupBox Header="Language Settings">
<StackPanel Spacing="10">
<TextBlock Text="This reflects the preferred application language as specified in OS settings."
TextWrapping="Wrap"
Opacity="0.7"
FontSize="12" />
<Grid ColumnDefinitions="180,*">
<TextBlock Grid.Column="0" Text="Preferred Language:" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Text="{Binding PreferredLanguage}" FontWeight="Bold" VerticalAlignment="Center" />
</Grid>
</StackPanel>
</GroupBox>
<GroupBox Header="Other Platform Settings">
<StackPanel Spacing="10">
<TextBlock Text="These settings are less commonly changed and are platform-specific."
TextWrapping="Wrap"
Opacity="0.7"
FontSize="12" />
<Grid ColumnDefinitions="180,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock Grid.Column="0" Grid.Row="0" Text="Hold Wait Duration:" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding HoldWaitDuration}" VerticalAlignment="Center" />
<TextBlock Grid.Column="0" Grid.Row="1" Text="Tap Size (Touch):" VerticalAlignment="Center" Margin="0,5,0,0" />
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding TapSizeTouch}" VerticalAlignment="Center" Margin="0,5,0,0" />
<TextBlock Grid.Column="0" Grid.Row="2" Text="Tap Size (Mouse):" VerticalAlignment="Center" Margin="0,5,0,0" />
<TextBlock Grid.Column="1" Grid.Row="2" Text="{Binding TapSizeMouse}" VerticalAlignment="Center" Margin="0,5,0,0" />
<TextBlock Grid.Column="0" Grid.Row="3" Text="Double-Tap Size (Touch):" VerticalAlignment="Center" Margin="0,5,0,0" />
<TextBlock Grid.Column="1" Grid.Row="3" Text="{Binding DoubleTapSizeTouch}" VerticalAlignment="Center" Margin="0,5,0,0" />
<TextBlock Grid.Column="0" Grid.Row="4" Text="Double-Tap Size (Mouse):" VerticalAlignment="Center" Margin="0,5,0,0" />
<TextBlock Grid.Column="1" Grid.Row="4" Text="{Binding DoubleTapSizeMouse}" VerticalAlignment="Center" Margin="0,5,0,0" />
<TextBlock Grid.Column="0" Grid.Row="5" Text="Double-Tap Time (Touch):" VerticalAlignment="Center" Margin="0,5,0,0" />
<TextBlock Grid.Column="1" Grid.Row="5" Text="{Binding DoubleTapTimeTouch}" VerticalAlignment="Center" Margin="0,5,0,0" />
<TextBlock Grid.Column="0" Grid.Row="6" Text="Double-Tap Time (Mouse):" VerticalAlignment="Center" Margin="0,5,0,0" />
<TextBlock Grid.Column="1" Grid.Row="6" Text="{Binding DoubleTapTimeMouse}" VerticalAlignment="Center" Margin="0,5,0,0" />
</Grid>
</StackPanel>
</GroupBox>
</StackPanel>
</ContentPage>

30
samples/ControlCatalog/Pages/PlatformSettingsPage.xaml.cs

@ -0,0 +1,30 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.VisualTree;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public partial class PlatformSettingsPage : ContentPage
{
private readonly PlatformSettingsViewModel _viewModel = new();
public PlatformSettingsPage()
{
InitializeComponent();
DataContext = _viewModel;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_viewModel.Subscribe(this.GetPlatformSettings() ?? Application.Current?.PlatformSettings);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_viewModel.Unsubscribe();
base.OnDetachedFromVisualTree(e);
}
}
}

21
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

@ -3,6 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="using:ControlCatalog.ViewModels"
xmlns:generic="clr-namespace:System.Collections.Generic;assembly=netstandard"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
@ -12,12 +13,28 @@
<DockPanel HorizontalAlignment="Stretch"
Height="600"
VerticalAlignment="Top">
<Label DockPanel.Dock="Top">A control that supports pull to refresh</Label>
<StackPanel DockPanel.Dock="Top">
<Label>A control that supports pull to refresh on touch inputs</Label>
<StackPanel Orientation="Horizontal" Margin="3" Spacing="5">
<TextBlock VerticalAlignment="Center">Used pull direction:</TextBlock>
<ComboBox x:Name="PullDirections" SelectedIndex="0">
<ComboBox.ItemsSource>
<generic:List x:TypeArguments="PullDirection">
<PullDirection>TopToBottom</PullDirection>
<PullDirection>LeftToRight</PullDirection>
<PullDirection>RightToLeft</PullDirection>
<PullDirection>BottomToTop</PullDirection>
</generic:List>
</ComboBox.ItemsSource>
</ComboBox>
<Button Content="Refresh" Name="RefreshButton"/>
</StackPanel>
</StackPanel>
<RefreshContainer Name="Refresh"
DockPanel.Dock="Bottom"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
PullDirection="TopToBottom"
PullDirection="{Binding #PullDirections.SelectedItem}"
RefreshRequested="RefreshContainerPage_RefreshRequested"
Margin="5">
<ListBox HorizontalAlignment="Stretch"

7
samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs

@ -11,11 +11,18 @@ namespace ControlCatalog.Pages
{
InitializeComponent();
RefreshButton.Click += RefreshButton_Click;
_viewModel = new RefreshContainerViewModel();
DataContext = _viewModel;
}
private void RefreshButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Refresh.RequestRefresh();
}
private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e)
{
var deferral = e.GetDeferral();

71
samples/ControlCatalog/Pages/TableViewPage.axaml

@ -0,0 +1,71 @@
<ContentPage xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:ControlCatalog.ViewModels"
xmlns:converter="clr-namespace:ControlCatalog.Converter"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ControlCatalog.Pages.TableViewPage"
x:DataType="viewModels:TableViewPageViewModel"
Header="TableView">
<ContentPage.Resources>
<converter:GdpToBrushConverter x:Key="GdpToBrushConverter" />
<converter:TableViewColumnWidthConverter x:Key="TableViewColumnWidthConverter" />
<ControlTheme x:Key="GdpCellTheme"
TargetType="TableViewCell"
BasedOn="{StaticResource {x:Type TableViewCell}}"
x:DataType="viewModels:TableViewPageViewModel+Country">
<Setter Property="Background" Value="{Binding GDP, Converter={StaticResource GdpToBrushConverter}}" />
</ControlTheme>
</ContentPage.Resources>
<DockPanel>
<StackPanel Orientation="Horizontal"
DockPanel.Dock="Top">
<CheckBox Name="UseStarSizes"
Content="Use star-sized columns"
IsChecked="True"
Margin="8" />
<CheckBox Name="CanUserResizeColumns"
Content="Allow column resizing"
IsChecked="True"
Margin="8" />
</StackPanel>
<TableView ItemsSource="{Binding Countries}"
CanUserResizeColumns="{Binding #CanUserResizeColumns.IsChecked}">
<TableView.Columns>
<TableViewColumn Header="Name"
Binding="{Binding Name}"
Width="{Binding #UseStarSizes.IsChecked, Converter={StaticResource TableViewColumnWidthConverter}, ConverterParameter=3}" />
<TableViewColumn Header="Region"
Binding="{Binding Region}"
Width="{Binding #UseStarSizes.IsChecked, Converter={StaticResource TableViewColumnWidthConverter}, ConverterParameter=2}" />
<TableViewColumn Header="Population"
Binding="{Binding Population, StringFormat=N0}"
Width="{Binding #UseStarSizes.IsChecked, Converter={StaticResource TableViewColumnWidthConverter}, ConverterParameter=1.5}"
HorizontalContentAlignment="Right" />
<TableViewColumn Header="Area"
Binding="{Binding Area, StringFormat=N0}"
Width="{Binding #UseStarSizes.IsChecked, Converter={StaticResource TableViewColumnWidthConverter}, ConverterParameter=1}"
HorizontalContentAlignment="Right" />
<TableViewColumn Header="GDP"
Binding="{Binding GDP, StringFormat=N0}"
Width="{Binding #UseStarSizes.IsChecked, Converter={StaticResource TableViewColumnWidthConverter}, ConverterParameter=1}"
CellTheme="{StaticResource GdpCellTheme}"
HorizontalContentAlignment="Right">
</TableViewColumn>
</TableView.Columns>
</TableView>
</DockPanel>
</ContentPage>

13
samples/ControlCatalog/Pages/TableViewPage.axaml.cs

@ -0,0 +1,13 @@
using Avalonia.Controls;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages;
public partial class TableViewPage : ContentPage
{
public TableViewPage()
{
InitializeComponent();
DataContext = new TableViewPageViewModel();
}
}

23
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -119,15 +119,20 @@
</StackPanel>
</Border>
<Border>
<SelectableTextBlock Margin="10" TextWrapping="Wrap">
This <Span FontWeight="Bold">is</Span> a
<Span Background="Silver" Foreground="Maroon">TextBlock</Span>
with <Span TextDecorations="Underline">several</Span>
<Span FontStyle="Italic">Span</Span> elements,
<Span Foreground="Blue">
using a <Bold>variety</Bold> of <Italic>styles</Italic>
</Span>.
</SelectableTextBlock>
<StackPanel Spacing="8">
<SelectableTextBlock Margin="10" TextWrapping="Wrap">
This <Span FontWeight="Bold">is</Span> a
<Span Background="Silver" Foreground="Maroon">TextBlock</Span>
with <Span TextDecorations="Underline">several</Span>
<Span FontStyle="Italic">Span</Span> elements,
<Span Foreground="Blue">
using a <Bold>variety</Bold> of <Italic>styles</Italic>
</Span>.
</SelectableTextBlock>
<SelectableTextBlock Text="Left aligned text" TextAlignment="Left" />
<SelectableTextBlock Text="Center aligned text" TextAlignment="Center" />
<SelectableTextBlock Text="Right aligned text and wrap text" TextAlignment="Right" TextWrapping="Wrap" />
</StackPanel>
</Border>
<Border>
<TextBlock FontFamily="Times New Roman">

5
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -57,9 +57,9 @@
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="8" Margin="8">
<TextBox AcceptsReturn="True" TextWrapping="Wrap" Width="200" Height="125"
<TextBox AcceptsReturn="True" TextWrapping="Wrap" Width="200" Height="125" TextInputOptions.Multiline="True"
Text="Multiline TextBox with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
<TextBox AcceptsReturn="True" Width="200" Height="125"
<TextBox AcceptsReturn="True" Width="200" Height="125" TextInputOptions.Multiline="True"
Text="Multiline TextBox with no TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
<TextBox Classes="clearButton" Text="Clear Content" Width="200" FontWeight="Normal" FontStyle="Normal" PlaceholderText="Placeholder" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
@ -80,6 +80,7 @@
Width="200"
Height="125"
LineHeight="32"
TextInputOptions.Multiline="True"
Text="Multiline TextBox with TextWrapping and increased LineHeight.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="8" Margin="8">

70
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@ -9,28 +9,58 @@
x:DataType="viewModels:MainWindowViewModel"
x:CompileBindings="True"
DataContext="{Binding $parent[Window].DataContext}">
<StackPanel>
<StackPanel Spacing="20">
<StackPanel
Spacing="10"
Margin="25"
IsEnabled="{OnFormFactor false, Desktop=true}">
<TextBlock Classes="h2"
<TextBlock Classes="h1"
Text="Desktop properties"
Margin="4" />
<CheckBox Content="Extend Client Area to Decorations"
IsChecked="{Binding ExtendClientAreaEnabled}" />
<DockPanel IsEnabled="{Binding ExtendClientAreaEnabled}">
<StackPanel IsVisible="{Binding ExtendClientAreaEnabled}"
Margin="28,0,0,0">
<Slider Minimum="-1"
Maximum="200"
Value="{Binding TitleBarHeight}"
Margin="8,-10" />
<Grid ColumnDefinitions="Auto,*,Auto">
</DockPanel>
<TextBlock Text="Height:"
VerticalAlignment="Center"
Grid.Column="0" />
<Slider Minimum="-1"
Maximum="200"
Value="{Binding TitleBarHeight}"
TickFrequency="1"
IsSnapToTickEnabled="True"
Margin="8,-10"
Grid.Column="1" />
<TextBlock Text="{Binding TitleBarHeight, StringFormat=F0}"
MinWidth="50"
VerticalAlignment="Center"
Grid.Column="2" />
</Grid>
<CheckBox Content="Title"
IsChecked="{Binding ShowTitle}" />
<CheckBox Content="Full Screen Button"
IsChecked="{Binding ShowFullScreenButton}" />
<CheckBox Content="Minimize Button"
IsChecked="{Binding ShowMinimizeButton}" />
<CheckBox Content="Maximize Button"
IsChecked="{Binding ShowMaximizeButton}" />
<CheckBox Content="Close Button"
IsChecked="{Binding ShowCloseButton}" />
</StackPanel>
<CheckBox Content="Can Resize"
IsChecked="{Binding CanResize}" />
@ -42,13 +72,27 @@
IsChecked="{Binding CanMaximize}"
IsEnabled="{Binding CanResize}" />
<DockPanel>
<Label Content="Corner Preference (Windows 11+): "
Margin="0,4"
DockPanel.Dock="Left" />
<ComboBox ItemsSource="{Binding Win32WindowCornerPreferences}"
SelectedItem="{Binding Win32WindowCornerPreference, Mode=TwoWay}"
HorizontalAlignment="Left"
MinWidth="220" />
</DockPanel>
</StackPanel>
<StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Mobile=true}">
<TextBlock Classes="h2" Text="Mobile properties" Margin="4" />
<StackPanel IsEnabled="{OnFormFactor false, Mobile=true}">
<TextBlock Classes="h1" 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}'}" />
<TextBlock Text="{Binding SafeAreaPadding, StringFormat='Safe Area Padding: {0}'}"
Margin="0,4" />
</StackPanel>
</StackPanel>

120
samples/ControlCatalog/Pages/WrapPanelPage.axaml

@ -0,0 +1,120 @@
<ContentPage xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="using:ControlCatalog.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ControlCatalog.Pages.WrapPanelPage"
x:DataType="viewModels:WrapPanelPageViewModel"
Header="WrapPanel">
<ContentPage.Styles>
<Style Selector="Border.item">
<Setter Property="Background" Value="#4C8BF5" />
<Setter Property="BorderBrush" Value="#1A4FB8" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="3" />
</Style>
<Style Selector="Border.item > TextBlock">
<Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</ContentPage.Styles>
<DockPanel>
<TextBlock Text="A panel which lays out its children in lines, wrapping to a new line when the current one is full."
Classes="h2"
DockPanel.Dock="Top" />
<Grid ColumnDefinitions="*,Auto">
<Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
BorderThickness="1"
Margin="0,10,10,10"
Grid.Column="0">
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel ItemsAlignment="{Binding ItemsAlignment}"
Orientation="{Binding Orientation}"
ItemWidth="{Binding EffectiveItemWidth}"
ItemHeight="{Binding EffectiveItemHeight}"
ItemSpacing="{Binding ItemSpacing}"
LineSpacing="{Binding LineSpacing}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Classes="item" Padding="{Binding Padding}">
<TextBlock Text="{Binding Number}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<StackPanel Width="220"
Margin="0,10,0,0"
Spacing="4"
VerticalAlignment="Top"
Grid.Column="1">
<TextBlock Text="ItemsAlignment" />
<ComboBox Name="ItemsAlignmentSelector"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ItemsAlignments}"
SelectedItem="{Binding ItemsAlignment}" />
<TextBlock Text="Orientation" Margin="0,8,0,0" />
<ComboBox Name="OrientationSelector"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Orientations}"
SelectedItem="{Binding Orientation}" />
<TextBlock Text="{Binding ItemSpacing, StringFormat='{}ItemSpacing: {0:F0}'}"
Margin="0,8,0,0" />
<Slider Name="ItemSpacingSlider"
Minimum="0"
Maximum="40"
TickFrequency="5"
IsSnapToTickEnabled="True"
Value="{Binding ItemSpacing}" />
<TextBlock Text="{Binding LineSpacing, StringFormat='{}LineSpacing: {0:F0}'}" />
<Slider Name="LineSpacingSlider"
Minimum="0"
Maximum="40"
TickFrequency="5"
IsSnapToTickEnabled="True"
Value="{Binding LineSpacing}" />
<CheckBox Name="ItemWidthCheckBox" Margin="0,8,0,0"
IsChecked="{Binding IsItemWidthEnabled}"
Content="{Binding ItemWidthValue, StringFormat='{}ItemWidth: {0:F0}'}" />
<Slider Name="ItemWidthSlider"
Minimum="20"
Maximum="250"
TickFrequency="10"
IsSnapToTickEnabled="True"
Value="{Binding ItemWidthValue}"
IsEnabled="{Binding IsItemWidthEnabled}" />
<CheckBox Name="ItemHeightCheckBox"
IsChecked="{Binding IsItemHeightEnabled}"
Content="{Binding ItemHeightValue, StringFormat='{}ItemHeight: {0:F0}'}" />
<Slider Name="ItemHeightSlider"
Minimum="20"
Maximum="250"
TickFrequency="10"
IsSnapToTickEnabled="True"
Value="{Binding ItemHeightValue}"
IsEnabled="{Binding IsItemHeightEnabled}" />
</StackPanel>
</Grid>
</DockPanel>
</ContentPage>

13
samples/ControlCatalog/Pages/WrapPanelPage.axaml.cs

@ -0,0 +1,13 @@
using Avalonia.Controls;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages;
public partial class WrapPanelPage : ContentPage
{
public WrapPanelPage()
{
InitializeComponent();
DataContext = new WrapPanelPageViewModel();
}
}

2
samples/ControlCatalog/ScrollPage.xaml

@ -15,7 +15,7 @@
HorizontalContentAlignment="Stretch"
IsVisible="False" />
<ScrollViewer Margin="20,20,5,0"
<ScrollViewer Padding="20"
HorizontalScrollBarVisibility="Disabled">
<ContentPresenter Name="PART_ContentPresenter"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"

3
samples/ControlCatalog/ViewModels/CursorPageViewModel.cs

@ -13,8 +13,7 @@ namespace ControlCatalog.ViewModels
{
public CursorPageViewModel()
{
StandardCursors = Enum.GetValues(typeof(StandardCursorType))
.Cast<StandardCursorType>()
StandardCursors = Enum.GetValues<StandardCursorType>()
.Select(x => new StandardCursorModel(x))
.ToList();

2
samples/ControlCatalog/ViewModels/DataValidationViewModel.cs

@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using MiniMvvm;
namespace ControlCatalog.ViewModels;
@ -11,6 +12,7 @@ public class DataValidationViewModel : ViewModelBase
[Required]
[EmailAddress]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Does not apply to 'string' type.")]
[MinLength(5)]
public string? DataAnnotationsSample
{

118
samples/ControlCatalog/ViewModels/FlexItemViewModel.cs

@ -0,0 +1,118 @@
using System;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using MiniMvvm;
namespace ControlCatalog.ViewModels
{
public sealed class FlexItemViewModel : ViewModelBase
{
internal const FlexAlignItems AlignSelfAuto = (FlexAlignItems)(-1);
private FlexAlignItems _alignSelf;
private bool _isSelected;
private bool _isVisible = true;
private FlexAlignItems _alignSelfItem = AlignSelfAuto;
private int _order;
private double _shrink = 1.0;
private double _grow;
private double _basisValue = 100.0;
private FlexBasisKind _basisKind;
private HorizontalAlignment _horizontalAlignment;
private VerticalAlignment _verticalAlignment;
public FlexItemViewModel(int value)
{
Value = value;
_alignSelf = AlignSelfItem == AlignSelfAuto ? default(FlexAlignItems) : AlignSelfItem;
var color = Random.Shared.Next();
Color = new SolidColorBrush((uint)color);
}
public int Value { get; }
public Brush Color { get; }
public bool IsSelected
{
get => _isSelected;
set => this.RaiseAndSetIfChanged(ref _isSelected, value);
}
public bool IsVisible
{
get => _isVisible;
set => this.RaiseAndSetIfChanged(ref _isVisible, value);
}
public FlexAlignItems AlignSelfItem
{
get => _alignSelfItem;
set
{
this.RaiseAndSetIfChanged(ref _alignSelfItem, value);
this.RaisePropertyChanged(nameof(AlignSelf));
}
}
public FlexAlignItems? AlignSelf => _alignSelf;
public int Order
{
get => _order;
set => this.RaiseAndSetIfChanged(ref _order, value);
}
public double Shrink
{
get => _shrink;
set => this.RaiseAndSetIfChanged(ref _shrink, value);
}
public double Grow
{
get => _grow;
set => this.RaiseAndSetIfChanged(ref _grow, value);
}
public double BasisValue
{
get => _basisValue;
set
{
this.RaiseAndSetIfChanged(ref _basisValue, value);
this.RaisePropertyChanged(nameof(Basis));
}
}
public FlexBasisKind BasisKind
{
get => _basisKind;
set
{
this.RaiseAndSetIfChanged(ref _basisKind, value);
this.RaisePropertyChanged(nameof(Basis));
}
}
public FlexBasis Basis => new(_basisValue, _basisKind);
public HorizontalAlignment HorizontalAlignment
{
get => _horizontalAlignment;
set => this.RaiseAndSetIfChanged(ref _horizontalAlignment, value);
}
public VerticalAlignment VerticalAlignment
{
get => _verticalAlignment;
set => this.RaiseAndSetIfChanged(ref _verticalAlignment, value);
}
}
}

126
samples/ControlCatalog/ViewModels/FlexViewModel.cs

@ -0,0 +1,126 @@
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Layout;
using MiniMvvm;
namespace ControlCatalog.ViewModels
{
public sealed class FlexViewModel : ViewModelBase
{
private readonly ObservableCollection<FlexItemViewModel> _numbers;
private FlexDirection _direction = FlexDirection.Row;
private FlexJustifyContent _justifyContent = FlexJustifyContent.FlexStart;
private FlexAlignItems _alignItems = FlexAlignItems.FlexStart;
private FlexAlignContent _alignContent = FlexAlignContent.FlexStart;
private FlexWrap _wrap = FlexWrap.Wrap;
private int _columnSpacing = 64;
private int _rowSpacing = 32;
private int _currentNumber = 41;
private FlexItemViewModel? _selectedItem;
public FlexViewModel()
{
_numbers = new ObservableCollection<FlexItemViewModel>(Enumerable.Range(1, 40).Select(x => new FlexItemViewModel(x)));
Numbers = new ReadOnlyObservableCollection<FlexItemViewModel>(_numbers);
AddItemCommand = MiniCommand.Create(AddItem);
RemoveItemCommand = MiniCommand.Create(RemoveItem);
}
public IEnumerable DirectionValues { get; } = Enum.GetValues<FlexDirection>();
public IEnumerable JustifyContentValues { get; } = Enum.GetValues<FlexJustifyContent>();
public IEnumerable AlignItemsValues { get; } = Enum.GetValues<FlexAlignItems>();
public IEnumerable AlignContentValues { get; } = Enum.GetValues<FlexAlignContent>();
public IEnumerable WrapValues { get; } = Enum.GetValues<FlexWrap>();
public IEnumerable FlexBasisKindValues { get; } = Enum.GetValues<FlexBasisKind>();
public IEnumerable HorizontalAlignmentValues { get; } = Enum.GetValues<HorizontalAlignment>();
public IEnumerable VerticalAlignmentValues { get; } = Enum.GetValues<VerticalAlignment>();
public IEnumerable AlignSelfValues { get; } = Enum.GetValues<FlexAlignItems>().Cast<FlexAlignItems>().Prepend(FlexItemViewModel.AlignSelfAuto);
public FlexDirection Direction
{
get => _direction;
set => this.RaiseAndSetIfChanged(ref _direction, value);
}
public FlexJustifyContent JustifyContent
{
get => _justifyContent;
set => this.RaiseAndSetIfChanged(ref _justifyContent, value);
}
public FlexAlignItems AlignItems
{
get => _alignItems;
set => this.RaiseAndSetIfChanged(ref _alignItems, value);
}
public FlexAlignContent AlignContent
{
get => _alignContent;
set => this.RaiseAndSetIfChanged(ref _alignContent, value);
}
public FlexWrap Wrap
{
get => _wrap;
set => this.RaiseAndSetIfChanged(ref _wrap, value);
}
public int ColumnSpacing
{
get => _columnSpacing;
set => this.RaiseAndSetIfChanged(ref _columnSpacing, value);
}
public int RowSpacing
{
get => _rowSpacing;
set => this.RaiseAndSetIfChanged(ref _rowSpacing, value);
}
public ReadOnlyObservableCollection<FlexItemViewModel> Numbers { get; }
public FlexItemViewModel? SelectedItem
{
get => _selectedItem;
set => this.RaiseAndSetIfChanged(ref _selectedItem, value);
}
public ICommand AddItemCommand { get; }
public ICommand RemoveItemCommand { get; }
private void AddItem() => _numbers.Add(new FlexItemViewModel(_currentNumber++));
private void RemoveItem()
{
if (SelectedItem is null)
{
return;
}
_numbers.Remove(SelectedItem);
SelectedItem.IsSelected = false;
SelectedItem = null;
}
}
}

60
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -1,8 +1,10 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Chrome;
using Avalonia.Dialogs;
using System;
using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices;
using Avalonia;
using MiniMvvm;
@ -123,6 +125,55 @@ namespace ControlCatalog.ViewModels
set { RaiseAndSetIfChanged(ref _selectedDecorationIndex, value); }
}
public TitleBarDecorations TitleBarDecorations
{
get;
set { RaiseAndSetIfChanged(ref field, value); }
} = TitleBarDecorations.All;
public bool ShowTitle
{
get => HasTitleBarDecoration(TitleBarDecorations.Title);
set => SetTitleBarDecoration(TitleBarDecorations.Title, value);
}
public bool ShowFullScreenButton
{
get => HasTitleBarDecoration(TitleBarDecorations.FullScreenButton);
set => SetTitleBarDecoration(TitleBarDecorations.FullScreenButton, value);
}
public bool ShowMinimizeButton
{
get => HasTitleBarDecoration(TitleBarDecorations.MinimizeButton);
set => SetTitleBarDecoration(TitleBarDecorations.MinimizeButton, value);
}
public bool ShowMaximizeButton
{
get => HasTitleBarDecoration(TitleBarDecorations.MaximizeButton);
set => SetTitleBarDecoration(TitleBarDecorations.MaximizeButton, value);
}
public bool ShowCloseButton
{
get => HasTitleBarDecoration(TitleBarDecorations.CloseButton);
set => SetTitleBarDecoration(TitleBarDecorations.CloseButton, value);
}
private bool HasTitleBarDecoration(TitleBarDecorations decoration)
=> (TitleBarDecorations & decoration) != 0;
private void SetTitleBarDecoration(TitleBarDecorations decoration, bool value, [CallerMemberName] string? propertyName = null)
{
var newDecorations = value ? TitleBarDecorations | decoration : TitleBarDecorations & ~decoration;
if (newDecorations == TitleBarDecorations)
return;
TitleBarDecorations = newDecorations;
RaisePropertyChanged(propertyName);
RaisePropertyChanged(nameof(TitleBarDecorations));
}
public MiniCommand AboutCommand { get; }
@ -141,5 +192,14 @@ namespace ControlCatalog.ViewModels
get => _validatedDateExample;
set => RaiseAndSetIfChanged(ref _validatedDateExample, value);
}
public Win32Properties.WindowCornerPreference[] Win32WindowCornerPreferences { get; } =
Enum.GetValues<Win32Properties.WindowCornerPreference>();
public Win32Properties.WindowCornerPreference Win32WindowCornerPreference
{
get;
set { RaiseAndSetIfChanged(ref field, value); }
}
}
}

4
samples/ControlCatalog/ViewModels/MainWindowViewModel_PageList.cs

@ -59,6 +59,7 @@ namespace ControlCatalog.ViewModels
new PageItem("Drag+Drop",() => new DragAndDropPage(), Icons.DragDrop, "Drag data within and between applications", "Interaction"),
new PageItem("DrawerPage",() => new DrawerDemoPage(), Icons.Drawer, "A page with a sliding navigation drawer", "Navigation & Pages"),
new PageItem("Expander",() => new ExpanderPage(), Icons.Expand, "A header that expands to reveal content", "Layout"),
new PageItem("Flex Panel",() => new FlexPage(), Icons.Grid, "TODO"),
new PageItem("Flyouts",() => new FlyoutsPage(), Icons.Flyout, "Lightweight popups anchored to controls", "Menus & Flyouts"),
new PageItem("Focus",() => new FocusPage(), Icons.Target, "Track and control keyboard focus", "Interaction"),
new PageItem("Gestures",() => new GesturePage(), Icons.Gesture, "Tap, scroll and pinch gesture recognition", "Interaction"),
@ -74,6 +75,7 @@ namespace ControlCatalog.ViewModels
new PageItem("OpenGL Lease",() => new OpenGlLeasePage(), Icons.Cube3D, "Low-level access to the OpenGL context", "Media & Graphics"),
new PageItem("PipsPager",() => new PipsPagerPage(), Icons.HorizontalDots, "Dot-style pager for paginated content", "Collections & Data"),
new PageItem("Platform Information",() => new PlatformInfoPage(), Icons.Info, "Runtime platform and capability info", "Window & Platform"),
new PageItem("Platform Settings",() => new PlatformSettingsPage(), Icons.Tune, "TODO"),
new PageItem("Pointers",() => new PointersPage(), Icons.Cursor, "Raw pointer input and capture", "Interaction"),
new PageItem("ProgressBar",() => new ProgressBarPage(), Icons.Progress, "Determinate and indeterminate progress", "Status & Feedback"),
new PageItem("RadioButton",() => new RadioButtonPage(), Icons.Radio, "Mutually exclusive option groups", "Basic Input"),
@ -85,6 +87,7 @@ namespace ControlCatalog.ViewModels
new PageItem("TabbedPage",() => new TabbedDemoPage(), Icons.Tab, "Tab-based page navigation", "Navigation & Pages"),
new PageItem("TabControl",() => new TabControlPage(), Icons.Tab, "Switch between tabbed content views", "Navigation & Pages"),
new PageItem("TabStrip",() => new TabStripPage(), Icons.Tab, "A standalone strip of selectable tabs", "Navigation & Pages"),
new PageItem("TableView",() => new TableViewPage(), Icons.Grid, "TODO"),
new PageItem("TextBox",() => new TextBoxPage(), Icons.TextInput, "Single- and multi-line text editing", "Text"),
new PageItem("TextBlock",() => new TextBlockPage(), Icons.TextInput, "Styled read-only text display", "Text"),
new PageItem("Theme Variants",() => new ThemePage(), Icons.Theme, "Switch between light and dark variants", "Window & Platform"),
@ -93,6 +96,7 @@ namespace ControlCatalog.ViewModels
new PageItem("TransitioningContentControl",() => new TransitioningContentControlPage(), Icons.Transition, "Animate between content changes", "Media & Graphics"),
new PageItem("TreeView",() => new TreeViewPage(), Icons.Tree, "Hierarchical data with expandable nodes", "Collections & Data"),
new PageItem("Viewbox",() => new ViewboxPage(), Icons.Viewbox, "Scale content to fit available space", "Layout"),
new PageItem("WrapPanel",() => new WrapPanelPage(), Icons.Layout, "TODO"),
new PageItem("Native Embed",() => new NativeEmbedPage(), Icons.Puzzle, "Host native platform controls", "Window & Platform"),
new PageItem("Window Customizations",() => new WindowCustomizationsPage(), Icons.Window, "Custom chrome, decorations and sizing", "Window & Platform"),
new PageItem("HeaderedContentControl",() => new HeaderedContentPage(), Icons.Header, "Content paired with a header", "Layout"),

80
samples/ControlCatalog/ViewModels/PlatformSettingsViewModel.cs

@ -0,0 +1,80 @@
using System;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Platform;
using MiniMvvm;
namespace ControlCatalog.ViewModels;
public class PlatformSettingsViewModel : ViewModelBase
{
private IPlatformSettings? _platformSettings;
private PlatformColorValues? _colorValues;
private string? _preferredLanguage;
public void Subscribe(IPlatformSettings? platformSettings)
{
_platformSettings = platformSettings;
if (platformSettings is not null)
{
OnColorValuesChanged(platformSettings, platformSettings.GetColorValues());
OnPreferredLanguageChanged(platformSettings, EventArgs.Empty);
platformSettings?.ColorValuesChanged += OnColorValuesChanged;
platformSettings?.PreferredApplicationLanguageChanged += OnPreferredLanguageChanged;
}
}
public void Unsubscribe()
{
if (_platformSettings != null)
{
_platformSettings.ColorValuesChanged -= OnColorValuesChanged;
_platformSettings.PreferredApplicationLanguageChanged -= OnPreferredLanguageChanged;
_platformSettings = null;
}
}
private void OnColorValuesChanged(object? sender, PlatformColorValues e)
{
_colorValues = e;
RaisePropertyChanged(nameof(ThemeVariant));
RaisePropertyChanged(nameof(ContrastPreference));
RaisePropertyChanged(nameof(AccentColor1));
RaisePropertyChanged(nameof(AccentColor2));
RaisePropertyChanged(nameof(AccentColor3));
}
private void OnPreferredLanguageChanged(object? sender, EventArgs e)
{
_preferredLanguage = ((IPlatformSettings)sender!).PreferredApplicationLanguage;
RaisePropertyChanged(nameof(PreferredLanguage));
}
public string PreferredLanguage => _preferredLanguage ?? "Not available";
public string ThemeVariant => _colorValues?.ThemeVariant.ToString() ?? "Not available";
public string ContrastPreference => _colorValues?.ContrastPreference.ToString() ?? "Not available";
public Color AccentColor1 => _colorValues?.AccentColor1 ?? Colors.Gray;
public Color AccentColor2 => _colorValues?.AccentColor2 ?? Colors.Gray;
public Color AccentColor3 => _colorValues?.AccentColor3 ?? Colors.Gray;
public string HoldWaitDuration => _platformSettings?.HoldWaitDuration.ToString() ?? "Not available";
public string TapSizeTouch => _platformSettings?.GetTapSize(PointerType.Touch).ToString() ?? "Not available";
public string TapSizeMouse => _platformSettings?.GetTapSize(PointerType.Mouse).ToString() ?? "Not available";
public string DoubleTapSizeTouch => _platformSettings?.GetDoubleTapSize(PointerType.Touch).ToString() ?? "Not available";
public string DoubleTapSizeMouse => _platformSettings?.GetDoubleTapSize(PointerType.Mouse).ToString() ?? "Not available";
public string DoubleTapTimeTouch => _platformSettings?.GetDoubleTapTime(PointerType.Touch).ToString() ?? "Not available";
public string DoubleTapTimeMouse => _platformSettings?.GetDoubleTapTime(PointerType.Mouse).ToString() ?? "Not available";
}

2
samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs

@ -1,8 +1,6 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using ControlCatalog.Pages;
using MiniMvvm;
namespace ControlCatalog.ViewModels

246
samples/ControlCatalog/ViewModels/TableViewPageViewModel.cs

@ -0,0 +1,246 @@
using System.Collections.ObjectModel;
using MiniMvvm;
namespace ControlCatalog.ViewModels;
public sealed class TableViewPageViewModel : ViewModelBase
{
public ObservableCollection<Country> Countries { get; } =
[
new("Afghanistan", "ASIA (EX. NEAR EAST)", 31056997, 647500, 700),
new("Albania", "EASTERN EUROPE", 3581655, 28748, 4500),
new("Algeria", "NORTHERN AFRICA", 32930091, 2381740, 6000),
new("American Samoa", "OCEANIA", 57794, 199, 8000),
new("Andorra", "WESTERN EUROPE", 71201, 468, 19000),
new("Angola", "SUB-SAHARAN AFRICA", 12127071, 1246700, 1900),
new("Anguilla", "LATIN AMER. & CARIB", 13477, 102, 8600),
new("Antigua & Barbuda", "LATIN AMER. & CARIB", 69108, 443, 11000),
new("Argentina", "LATIN AMER. & CARIB", 39921833, 2766890, 11200),
new("Armenia", "C.W. OF IND. STATES", 2976372, 29800, 3500),
new("Aruba", "LATIN AMER. & CARIB", 71891, 193, 28000),
new("Australia", "OCEANIA", 20264082, 7686850, 29000),
new("Austria", "WESTERN EUROPE", 8192880, 83870, 30000),
new("Azerbaijan", "C.W. OF IND. STATES", 7961619, 86600, 3400),
new("The Bahamas", "LATIN AMER. & CARIB", 303770, 13940, 16700),
new("Bahrain", "NEAR EAST", 698585, 665, 16900),
new("Bangladesh", "ASIA (EX. NEAR EAST)", 147365352, 144000, 1900),
new("Barbados", "LATIN AMER. & CARIB", 279912, 431, 15700),
new("Belarus", "C.W. OF IND. STATES", 10293011, 207600, 6100),
new("Belgium", "WESTERN EUROPE", 10379067, 30528, 29100),
new("Belize", "LATIN AMER. & CARIB", 287730, 22966, 4900),
new("Benin", "SUB-SAHARAN AFRICA", 7862944, 112620, 1100),
new("Bermuda", "NORTHERN AMERICA", 65773, 53, 36000),
new("Bhutan", "ASIA (EX. NEAR EAST)", 2279723, 47000, 1300),
new("Bolivia", "LATIN AMER. & CARIB", 8989046, 1098580, 2400),
new("Bosnia & Herzegovina", "EASTERN EUROPE", 4498976, 51129, 6100),
new("Botswana", "SUB-SAHARAN AFRICA", 1639833, 600370, 9000),
new("Brazil", "LATIN AMER. & CARIB", 188078227, 8511965, 7600),
new("British Virgin Is.", "LATIN AMER. & CARIB", 23098, 153, 16000),
new("Brunei", "ASIA (EX. NEAR EAST)", 379444, 5770, 18600),
new("Bulgaria", "EASTERN EUROPE", 7385367, 110910, 7600),
new("Burkina Faso", "SUB-SAHARAN AFRICA", 13902972, 274200, 1100),
new("Burma", "ASIA (EX. NEAR EAST)", 47382633, 678500, 1800),
new("Burundi", "SUB-SAHARAN AFRICA", 8090068, 27830, 600),
new("Cambodia", "ASIA (EX. NEAR EAST)", 13881427, 181040, 1900),
new("Cameroon", "SUB-SAHARAN AFRICA", 17340702, 475440, 1800),
new("Canada", "NORTHERN AMERICA", 33098932, 9984670, 29800),
new("Cape Verde", "SUB-SAHARAN AFRICA", 420979, 4033, 1400),
new("Cayman Islands", "LATIN AMER. & CARIB", 45436, 262, 35000),
new("Central African Rep.", "SUB-SAHARAN AFRICA", 4303356, 622984, 1100),
new("Chad", "SUB-SAHARAN AFRICA", 9944201, 1284000, 1200),
new("Chile", "LATIN AMER. & CARIB", 16134219, 756950, 9900),
new("China", "ASIA (EX. NEAR EAST)", 1313973713, 9596960, 5000),
new("Colombia", "LATIN AMER. & CARIB", 43593035, 1138910, 6300),
new("Comoros", "SUB-SAHARAN AFRICA", 690948, 2170, 700),
new("Congo, Dem.Rep.", "SUB - SAHARAN AFRICA", 62660551, 2345410, 700),
new("Congo, Repub.of the", "SUB - SAHARAN AFRICA", 3702314, 342000, 700),
new("Cook Islands", "OCEANIA", 21388, 240, 5000),
new("Costa Rica", "LATIN AMER. & CARIB", 4075261, 51100, 9100),
new("Cote d'Ivoire", "SUB-SAHARAN AFRICA", 17654843, 322460, 1400),
new("Croatia", "EASTERN EUROPE", 4494749, 56542, 10600),
new("Cuba", "LATIN AMER. & CARIB", 11382820, 110860, 2900),
new("Cyprus", "NEAR EAST", 784301, 9250, 19200),
new("Czech Republic", "EASTERN EUROPE", 10235455, 78866, 15700),
new("Denmark", "WESTERN EUROPE", 5450661, 43094, 31100),
new("Djibouti", "SUB-SAHARAN AFRICA", 486530, 23000, 1300),
new("Dominica", "LATIN AMER. & CARIB", 68910, 754, 5400),
new("Dominican Republic", "LATIN AMER. & CARIB", 9183984, 48730, 6000),
new("East Timor", "ASIA (EX. NEAR EAST)", 1062777, 15007, 500),
new("Ecuador", "LATIN AMER. & CARIB", 13547510, 283560, 3300),
new("Egypt", "NORTHERN AFRICA", 78887007, 1001450, 4000),
new("El Salvador", "LATIN AMER. & CARIB", 6822378, 21040, 4800),
new("Equatorial Guinea", "SUB-SAHARAN AFRICA", 540109, 28051, 2700),
new("Eritrea", "SUB-SAHARAN AFRICA", 4786994, 121320, 700),
new("Estonia", "BALTICS", 1324333, 45226, 12300),
new("Ethiopia", "SUB-SAHARAN AFRICA", 74777981, 1127127, 700),
new("Faroe Islands", "WESTERN EUROPE", 47246, 1399, 22000),
new("Fiji", "OCEANIA", 905949, 18270, 5800),
new("Finland", "WESTERN EUROPE", 5231372, 338145, 27400),
new("France", "WESTERN EUROPE", 60876136, 547030, 27600),
new("French Guiana", "LATIN AMER. & CARIB", 199509, 91000, 8300),
new("French Polynesia", "OCEANIA", 274578, 4167, 17500),
new("Gabon", "SUB-SAHARAN AFRICA", 1424906, 267667, 5500),
new("Gambia, The", "SUB - SAHARAN AFRICA", 1641564, 11300, 1700),
new("Gaza Strip", "NEAR EAST", 1428757, 360, 600),
new("Georgia", "C.W. OF IND. STATES", 4661473, 69700, 2500),
new("Germany", "WESTERN EUROPE", 82422299, 357021, 27600),
new("Ghana", "SUB-SAHARAN AFRICA", 22409572, 239460, 2200),
new("Gibraltar", "WESTERN EUROPE", 27928, 7, 17500),
new("Greece", "WESTERN EUROPE", 10688058, 131940, 20000),
new("Greenland", "NORTHERN AMERICA", 56361, 2166086, 20000),
new("Grenada", "LATIN AMER. & CARIB", 89703, 344, 5000),
new("Guadeloupe", "LATIN AMER. & CARIB", 452776, 1780, 8000),
new("Guam", "OCEANIA", 171019, 541, 21000),
new("Guatemala", "LATIN AMER. & CARIB", 12293545, 108890, 4100),
new("Guernsey", "WESTERN EUROPE", 65409, 78, 20000),
new("Guinea", "SUB-SAHARAN AFRICA", 9690222, 245857, 2100),
new("Guinea-Bissau", "SUB-SAHARAN AFRICA", 1442029, 36120, 800),
new("Guyana", "LATIN AMER. & CARIB", 767245, 214970, 4000),
new("Haiti", "LATIN AMER. & CARIB", 8308504, 27750, 1600),
new("Honduras", "LATIN AMER. & CARIB", 7326496, 112090, 2600),
new("Hong Kong", "ASIA (EX. NEAR EAST)", 6940432, 1092, 28800),
new("Hungary", "EASTERN EUROPE", 9981334, 93030, 13900),
new("Iceland", "WESTERN EUROPE", 299388, 103000, 30900),
new("India", "ASIA (EX. NEAR EAST)", 1095351995, 3287590, 2900),
new("Indonesia", "ASIA (EX. NEAR EAST)", 245452739, 1919440, 3200),
new("Iran", "ASIA (EX. NEAR EAST)", 68688433, 1648000, 7000),
new("Iraq", "NEAR EAST", 26783383, 437072, 1500),
new("Ireland", "WESTERN EUROPE", 4062235, 70280, 29600),
new("Isle of Man", "WESTERN EUROPE", 75441, 572, 21000),
new("Israel", "NEAR EAST", 6352117, 20770, 19800),
new("Italy", "WESTERN EUROPE", 58133509, 301230, 26700),
new("Jamaica", "LATIN AMER. & CARIB", 2758124, 10991, 3900),
new("Japan", "ASIA (EX. NEAR EAST)", 127463611, 377835, 28200),
new("Jersey", "WESTERN EUROPE", 91084, 116, 24800),
new("Jordan", "NEAR EAST", 5906760, 92300, 4300),
new("Kazakhstan", "C.W. OF IND. STATES", 15233244, 2717300, 6300),
new("Kenya", "SUB-SAHARAN AFRICA", 34707817, 582650, 1000),
new("Kiribati", "OCEANIA", 105432, 811, 800),
new("North Korea", "ASIA(EX.NEAR EAST)", 23113019, 120540, 1300),
new("South Korea", "ASIA(EX.NEAR EAST)", 48846823, 98480, 17800),
new("Kuwait", "NEAR EAST", 2418393, 17820, 19000),
new("Kyrgyzstan", "C.W. OF IND. STATES", 5213898, 198500, 1600),
new("Laos", "ASIA (EX. NEAR EAST)", 6368481, 236800, 1700),
new("Latvia", "BALTICS", 2274735, 64589, 10200),
new("Lebanon", "NEAR EAST", 3874050, 10400, 4800),
new("Lesotho", "SUB-SAHARAN AFRICA", 2022331, 30355, 3000),
new("Liberia", "SUB-SAHARAN AFRICA", 3042004, 111370, 1000),
new("Libya", "NORTHERN AFRICA", 5900754, 1759540, 6400),
new("Liechtenstein", "WESTERN EUROPE", 33987, 160, 25000),
new("Lithuania", "BALTICS", 3585906, 65200, 11400),
new("Luxembourg", "WESTERN EUROPE", 474413, 2586, 55100),
new("Macau", "ASIA (EX. NEAR EAST)", 453125, 28, 19400),
new("Macedonia", "EASTERN EUROPE", 2050554, 25333, 6700),
new("Madagascar", "SUB-SAHARAN AFRICA", 18595469, 587040, 800),
new("Malawi", "SUB-SAHARAN AFRICA", 13013926, 118480, 600),
new("Malaysia", "ASIA (EX. NEAR EAST)", 24385858, 329750, 9000),
new("Maldives", "ASIA (EX. NEAR EAST)", 359008, 300, 3900),
new("Mali", "SUB-SAHARAN AFRICA", 11716829, 1240000, 900),
new("Malta", "WESTERN EUROPE", 400214, 316, 17700),
new("Marshall Islands", "OCEANIA", 60422, 11854, 1600),
new("Martinique", "LATIN AMER. & CARIB", 436131, 1100, 14400),
new("Mauritania", "SUB-SAHARAN AFRICA", 3177388, 1030700, 1800),
new("Mauritius", "SUB-SAHARAN AFRICA", 1240827, 2040, 11400),
new("Mayotte", "SUB-SAHARAN AFRICA", 201234, 374, 2600),
new("Mexico", "LATIN AMER. & CARIB", 107449525, 1972550, 9000),
new("Micronesia, Fed.St.", "OCEANIA", 108004, 702, 2000),
new("Moldova", "C.W. OF IND. STATES", 4466706, 33843, 1800),
new("Monaco", "WESTERN EUROPE", 32543, 2, 27000),
new("Mongolia", "ASIA (EX. NEAR EAST)", 2832224, 1564116, 1800),
new("Montserrat", "LATIN AMER. & CARIB", 9439, 102, 3400),
new("Morocco", "NORTHERN AFRICA", 33241259, 446550, 4000),
new("Mozambique", "SUB-SAHARAN AFRICA", 19686505, 801590, 1200),
new("Namibia", "SUB-SAHARAN AFRICA", 2044147, 825418, 7200),
new("Nauru", "OCEANIA", 13287, 21, 5000),
new("Nepal", "ASIA (EX. NEAR EAST)", 28287147, 147181, 1400),
new("Netherlands", "WESTERN EUROPE", 16491461, 41526, 28600),
new("Netherlands Antilles", "LATIN AMER. & CARIB", 221736, 960, 11400),
new("New Caledonia", "OCEANIA", 219246, 19060, 15000),
new("New Zealand", "OCEANIA", 4076140, 268680, 21600),
new("Nicaragua", "LATIN AMER. & CARIB", 5570129, 129494, 2300),
new("Niger", "SUB-SAHARAN AFRICA", 12525094, 1267000, 800),
new("Nigeria", "SUB-SAHARAN AFRICA", 131859731, 923768, 900),
new("N. Mariana Islands", "OCEANIA", 82459, 477, 12500),
new("Norway", "WESTERN EUROPE", 4610820, 323802, 37800),
new("Oman", "NEAR EAST", 3102229, 212460, 13100),
new("Pakistan", "ASIA (EX. NEAR EAST)", 165803560, 803940, 2100),
new("Palau", "OCEANIA", 20579, 458, 9000),
new("Panama", "LATIN AMER. & CARIB", 3191319, 78200, 6300),
new("Papua New Guinea", "OCEANIA", 5670544, 462840, 2200),
new("Paraguay", "LATIN AMER. & CARIB", 6506464, 406750, 4700),
new("Peru", "LATIN AMER. & CARIB", 28302603, 1285220, 5100),
new("Philippines", "ASIA (EX. NEAR EAST)", 89468677, 300000, 4600),
new("Poland", "EASTERN EUROPE", 38536869, 312685, 11100),
new("Portugal", "WESTERN EUROPE", 10605870, 92391, 18000),
new("Puerto Rico", "LATIN AMER. & CARIB", 3927188, 13790, 16800),
new("Qatar", "NEAR EAST", 885359, 11437, 21500),
new("Reunion", "SUB-SAHARAN AFRICA", 787584, 2517, 5800),
new("Romania", "EASTERN EUROPE", 22303552, 237500, 7000),
new("Russia", "C.W. OF IND. STATES", 142893540, 17075200, 8900),
new("Rwanda", "SUB-SAHARAN AFRICA", 8648248, 26338, 1300),
new("Saint Helena", "SUB-SAHARAN AFRICA", 7502, 413, 2500),
new("Saint Kitts & Nevis", "LATIN AMER. & CARIB", 39129, 261, 8800),
new("Saint Lucia", "LATIN AMER. & CARIB", 168458, 616, 5400),
new("St Pierre & Miquelon", "NORTHERN AMERICA", 7026, 242, 6900),
new("Saint Vincent and the Grenadines", "LATIN AMER. & CARIB", 117848, 389, 2900),
new("Samoa", "OCEANIA", 176908, 2944, 5600),
new("San Marino", "WESTERN EUROPE", 29251, 61, 34600),
new("Sao Tome & Principe", "SUB-SAHARAN AFRICA", 193413, 1001, 1200),
new("Saudi Arabia", "NEAR EAST", 27019731, 1960582, 11800),
new("Senegal", "SUB-SAHARAN AFRICA", 11987121, 196190, 1600),
new("Serbia", "EASTERN EUROPE", 9396411, 88361, 2200),
new("Seychelles", "SUB-SAHARAN AFRICA", 81541, 455, 7800),
new("Sierra Leone", "SUB-SAHARAN AFRICA", 6005250, 71740, 500),
new("Singapore", "ASIA (EX. NEAR EAST)", 4492150, 693, 23700),
new("Slovakia", "EASTERN EUROPE", 5439448, 48845, 13300),
new("Slovenia", "EASTERN EUROPE", 2010347, 20273, 19000),
new("Solomon Islands", "OCEANIA", 552438, 28450, 1700),
new("Somalia", "SUB-SAHARAN AFRICA", 8863338, 637657, 500),
new("South Africa", "SUB-SAHARAN AFRICA", 44187637, 1219912, 10700),
new("Spain", "WESTERN EUROPE", 40397842, 504782, 22000),
new("Sri Lanka", "ASIA (EX. NEAR EAST)", 20222240, 65610, 3700),
new("Sudan", "SUB-SAHARAN AFRICA", 41236378, 2505810, 1900),
new("Suriname", "LATIN AMER. & CARIB", 439117, 163270, 4000),
new("Swaziland", "SUB-SAHARAN AFRICA", 1136334, 17363, 4900),
new("Sweden", "WESTERN EUROPE", 9016596, 449964, 26800),
new("Switzerland", "WESTERN EUROPE", 7523934, 41290, 32700),
new("Syria", "NEAR EAST", 18881361, 185180, 3300),
new("Taiwan", "ASIA (EX. NEAR EAST)", 23036087, 35980, 23400),
new("Tajikistan", "C.W. OF IND. STATES", 7320815, 143100, 1000),
new("Tanzania", "SUB-SAHARAN AFRICA", 37445392, 945087, 600),
new("Thailand", "ASIA (EX. NEAR EAST)", 64631595, 514000, 7400),
new("Togo", "SUB-SAHARAN AFRICA", 5548702, 56785, 1500),
new("Tonga", "OCEANIA", 114689, 748, 2200),
new("Trinidad & Tobago", "LATIN AMER. & CARIB", 1065842, 5128, 9500),
new("Tunisia", "NORTHERN AFRICA", 10175014, 163610, 6900),
new("Turkey", "NEAR EAST", 70413958, 780580, 6700),
new("Turkmenistan", "C.W. OF IND. STATES", 5042920, 488100, 5800),
new("Turks & Caicos Is", "LATIN AMER. & CARIB", 21152, 430, 9600),
new("Tuvalu", "OCEANIA", 11810, 26, 1100),
new("Uganda", "SUB-SAHARAN AFRICA", 28195754, 236040, 1400),
new("Ukraine", "C.W. OF IND. STATES", 46710816, 603700, 5400),
new("United Arab Emirates", "NEAR EAST", 2602713, 82880, 23200),
new("United Kingdom", "WESTERN EUROPE", 60609153, 244820, 27700),
new("United States", "NORTHERN AMERICA", 298444215, 9631420, 37800),
new("Uruguay", "LATIN AMER. & CARIB", 3431932, 176220, 12800),
new("Uzbekistan", "C.W. OF IND. STATES", 27307134, 447400, 1700),
new("Vanuatu", "OCEANIA", 208869, 12200, 2900),
new("Venezuela", "LATIN AMER. & CARIB", 25730435, 912050, 4800),
new("Vietnam", "ASIA (EX. NEAR EAST)", 84402966, 329560, 2500),
new("Virgin Islands", "LATIN AMER. & CARIB", 108605, 1910, 17200),
new("Wallis and Futuna", "OCEANIA", 16025, 274, 3700),
new("West Bank", "NEAR EAST", 2460492, 5860, 800),
new("Yemen", "NEAR EAST", 21456188, 527970, 800),
new("Zambia", "SUB-SAHARAN AFRICA", 11502010, 752614, 800),
new("Zimbabwe", "SUB-SAHARAN AFRICA", 12236805, 390580, 1900)
];
public sealed class Country(string name, string region, int population, int area, int gdp)
{
public string Name { get; set; } = name;
public string Region { get; set; } = region;
public int Population { get; set; } = population;
public int Area { get; set; } = area; // Square miles
public int GDP { get; set; } = gdp;
}
}

105
samples/ControlCatalog/ViewModels/WrapPanelPageViewModel.cs

@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using MiniMvvm;
namespace ControlCatalog.ViewModels;
public class WrapPanelPageViewModel : ViewModelBase
{
public WrapPanelItemsAlignment[] ItemsAlignments { get; } = Enum.GetValues<WrapPanelItemsAlignment>();
public Orientation[] Orientations { get; } = Enum.GetValues<Orientation>();
public IReadOnlyList<WrapPanelItemViewModel> Items { get; } = CreateItems();
public WrapPanelItemsAlignment ItemsAlignment
{
get;
set => RaiseAndSetIfChanged(ref field, value);
} = WrapPanelItemsAlignment.Start;
public Orientation Orientation
{
get;
set => RaiseAndSetIfChanged(ref field, value);
} = Orientation.Horizontal;
public double ItemSpacing
{
get;
set => RaiseAndSetIfChanged(ref field, value);
} = 5;
public double LineSpacing
{
get;
set => RaiseAndSetIfChanged(ref field, value);
} = 5;
public bool IsItemWidthEnabled
{
get;
set
{
if (RaiseAndSetIfChanged(ref field, value))
RaisePropertyChanged(nameof(EffectiveItemWidth));
}
}
public double ItemWidthValue
{
get;
set
{
if (RaiseAndSetIfChanged(ref field, value))
RaisePropertyChanged(nameof(EffectiveItemWidth));
}
} = 140;
public bool IsItemHeightEnabled
{
get;
set
{
if (RaiseAndSetIfChanged(ref field, value))
RaisePropertyChanged(nameof(EffectiveItemHeight));
}
}
public double ItemHeightValue
{
get;
set
{
if (RaiseAndSetIfChanged(ref field, value))
RaisePropertyChanged(nameof(EffectiveItemHeight));
}
} = 90;
public double EffectiveItemWidth
=> IsItemWidthEnabled ? ItemWidthValue : double.NaN;
public double EffectiveItemHeight
=> IsItemHeightEnabled ? ItemHeightValue : double.NaN;
private static WrapPanelItemViewModel[] CreateItems()
{
var random = new Random(42);
var items = new WrapPanelItemViewModel[50];
for (var i = 0; i < items.Length; ++i)
items[i] = new WrapPanelItemViewModel(i + 1, new Thickness(random.Next(15, 56), random.Next(8, 31)));
return items;
}
}
public class WrapPanelItemViewModel(int number, Thickness padding)
{
public int Number { get; } = number;
public Thickness Padding { get; } = padding;
}

16
samples/ControlGallery.WinUI/App.xaml

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Application
x:Class="ControlGallery.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ControlGallery.WinUI">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>

40
samples/ControlGallery.WinUI/App.xaml.cs

@ -0,0 +1,40 @@
using global::Avalonia;
using global::Avalonia.Controls;
using global::Avalonia.Controls.ApplicationLifetimes;
using global::Avalonia.Skia;
using global::Avalonia.Win32;
using WinUIApplication = Microsoft.UI.Xaml.Application;
using LaunchActivatedEventArgs = Microsoft.UI.Xaml.LaunchActivatedEventArgs;
using Window = Microsoft.UI.Xaml.Window;
namespace ControlGallery.WinUI
{
public partial class App : WinUIApplication
{
private Window? _window;
internal static SingleViewLifetime Lifetime { get; } = new();
public App()
{
InitializeComponent();
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
AppBuilder.Configure<ControlCatalog.App>()
.UseWin32()
.UseSkia()
.UseHarfBuzz()
.SetupWithLifetime(Lifetime);
_window = new MainWindow();
_window.Activate();
}
}
internal sealed class SingleViewLifetime : ISingleViewApplicationLifetime
{
public Control? MainView { get; set; }
}
}

BIN
samples/ControlGallery.WinUI/Assets/LockScreenLogo.scale-200.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

BIN
samples/ControlGallery.WinUI/Assets/SplashScreen.scale-200.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
samples/ControlGallery.WinUI/Assets/Square150x150Logo.scale-200.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
samples/ControlGallery.WinUI/Assets/Square44x44Logo.scale-200.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

BIN
samples/ControlGallery.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

BIN
samples/ControlGallery.WinUI/Assets/StoreLogo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

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

Loading…
Cancel
Save