Browse Source

Merge remote-tracking branch 'origin/main' into pr/21975

# Conflicts:
#	src/Avalonia.Controls/VirtualizingStackPanel.cs
pull/21975/head
grokys 2 months ago
parent
commit
71ad36c9b9
  1. 12
      .editorconfig
  2. 5
      .ncrunch/Avalonia.Controls.ColorPicker.v3.ncrunchproject
  3. 8
      .ncrunch/Avalonia.DBus.Generators.v3.ncrunchproject
  4. 5
      .ncrunch/Avalonia.FreeDesktop.AtSpi.v3.ncrunchproject
  5. 1
      Avalonia.Desktop.slnf
  6. 1
      Avalonia.slnx
  7. 1
      Directory.Build.props
  8. 8
      Directory.Packages.props
  9. 28
      api/Avalonia.nupkg.xml
  10. BIN
      build/Assets/Icon.png
  11. BIN
      build/Assets/icon-32.png
  12. BIN
      build/Assets/icon.ico
  13. 4
      build/Assets/icon.svg
  14. 4
      build/SharedVersion.props
  15. 195
      native/Avalonia.Native/src/OSX/AvnView.mm
  16. 46
      native/Avalonia.Native/src/OSX/PlatformSettings.mm
  17. 14
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  18. 8
      packages/Avalonia/AvaloniaRules.Project.xml
  19. 2
      readme.md
  20. 4
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  21. BIN
      samples/ControlCatalog.Browser/wwwroot/favicon.ico
  22. 2
      samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj
  23. 4
      samples/ControlCatalog/App.xaml
  24. BIN
      samples/ControlCatalog/Assets/Icon.png
  25. BIN
      samples/ControlCatalog/Assets/avalonia-32.png
  26. BIN
      samples/ControlCatalog/Assets/banner-bg.png
  27. BIN
      samples/ControlCatalog/Assets/banner-logo.png
  28. BIN
      samples/ControlCatalog/Assets/logo1.png
  29. BIN
      samples/ControlCatalog/Assets/logo2.png
  30. BIN
      samples/ControlCatalog/Assets/logo3.png
  31. BIN
      samples/ControlCatalog/Assets/logo4.png
  32. BIN
      samples/ControlCatalog/Assets/test_icon.ico
  33. 21
      samples/ControlCatalog/ControlCatalog.csproj
  34. 24
      samples/ControlCatalog/Converter/FlexDemoNumberToThicknessConverter.cs
  35. 3
      samples/ControlCatalog/Icons.cs
  36. 19
      samples/ControlCatalog/MainView.xaml
  37. 6
      samples/ControlCatalog/MainView.xaml.cs
  38. 7
      samples/ControlCatalog/MainWindow.xaml
  39. 10
      samples/ControlCatalog/Models/HomeSection.cs
  40. 53
      samples/ControlCatalog/Models/PageItem.cs
  41. 3
      samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
  42. 11
      samples/ControlCatalog/Pages/CalendarPage.xaml
  43. 4
      samples/ControlCatalog/Pages/ComboBoxPage.xaml
  44. 28
      samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs
  45. 218
      samples/ControlCatalog/Pages/FlexPage.axaml
  46. 37
      samples/ControlCatalog/Pages/FlexPage.axaml.cs
  47. 152
      samples/ControlCatalog/Pages/HomePage.xaml
  48. 82
      samples/ControlCatalog/Pages/HomePage.xaml.cs
  49. 1
      samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml
  50. 136
      samples/ControlCatalog/Pages/PlatformSettingsPage.xaml
  51. 30
      samples/ControlCatalog/Pages/PlatformSettingsPage.xaml.cs
  52. 1
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  53. 7
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  54. 233
      samples/ControlCatalog/Pages/ScreenPage.cs
  55. 75
      samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavBar.cs
  56. 6
      samples/ControlCatalog/Pages/Transitions/CardStackPageTransition.cs
  57. 59
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  58. 2
      samples/ControlCatalog/ScrollPage.xaml
  59. 2
      samples/ControlCatalog/ViewModels/CursorPageViewModel.cs
  60. 118
      samples/ControlCatalog/ViewModels/FlexItemViewModel.cs
  61. 126
      samples/ControlCatalog/ViewModels/FlexViewModel.cs
  62. 255
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  63. 354
      samples/ControlCatalog/ViewModels/MainWindowViewModel_PageList.cs
  64. 80
      samples/ControlCatalog/ViewModels/PlatformSettingsViewModel.cs
  65. 2
      samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
  66. 5
      samples/Directory.Build.props
  67. BIN
      samples/Generators.Sandbox/Assets/Icon.png
  68. BIN
      samples/IntegrationTestApp/Assets/icon.ico
  69. 1
      samples/IntegrationTestApp/IntegrationTestApp.csproj
  70. 1
      samples/IntegrationTestApp/MainWindow.axaml
  71. 1
      samples/IntegrationTestApp/MainWindow.axaml.cs
  72. 45
      samples/IntegrationTestApp/Pages/KeyboardPage.axaml
  73. 63
      samples/IntegrationTestApp/Pages/KeyboardPage.axaml.cs
  74. 4
      samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj
  75. 1
      samples/SafeAreaDemo.Desktop/SafeAreaDemo.Desktop.csproj
  76. BIN
      samples/SafeAreaDemo/Assets/avalonia-logo.ico
  77. 129
      samples/SafeAreaDemo/Views/MainView.xaml
  78. 1
      samples/SafeAreaDemo/Views/MainWindow.xaml
  79. BIN
      samples/SingleProjectSandbox/Platforms/Browser/wwwroot/favicon.ico
  80. 1
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  81. 45
      src/Android/Avalonia.Android/AvaloniaAccessHelper.cs
  82. 20
      src/Android/Avalonia.Android/AvaloniaView.cs
  83. 22
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  84. 131
      src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs
  85. 25
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  86. 67
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
  87. 9
      src/Avalonia.Base/Animation/AnimationInstance`1.cs
  88. 19
      src/Avalonia.Base/Animation/Animators/Animator`1.cs
  89. 4
      src/Avalonia.Base/Animation/Animators/TransformAnimator.cs
  90. 6
      src/Avalonia.Base/Animation/CrossFade.cs
  91. 11
      src/Avalonia.Base/Animation/DisposeAnimationInstanceSubject.cs
  92. 6
      src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
  93. 50
      src/Avalonia.Base/Data/Converters/ObjectConverters.cs
  94. 60
      src/Avalonia.Base/GeneratedAttachedPropertyAttribute.cs
  95. 41
      src/Avalonia.Base/GeneratedDirectPropertyAttribute.cs
  96. 66
      src/Avalonia.Base/GeneratedStyledPropertyAttribute.cs
  97. 6
      src/Avalonia.Base/Input/MouseDevice.cs
  98. 9
      src/Avalonia.Base/Input/PenDevice.cs
  99. 53
      src/Avalonia.Base/Input/Pointer.cs
  100. 12
      src/Avalonia.Base/Input/TouchDevice.cs

12
.editorconfig

@ -199,6 +199,18 @@ dotnet_diagnostic.AVP1032.severity = warning
dotnet_diagnostic.AVP1040.severity = warning
dotnet_diagnostic.AVA2001.severity = warning
# Avalonia property source generator (GeneratedPropertyAnalyzer)
dotnet_diagnostic.AVP2001.severity = error
dotnet_diagnostic.AVP2002.severity = error
dotnet_diagnostic.AVP2003.severity = error
dotnet_diagnostic.AVP2004.severity = error
dotnet_diagnostic.AVP2005.severity = error
dotnet_diagnostic.AVP2006.severity = error
dotnet_diagnostic.AVP2007.severity = error
dotnet_diagnostic.AVP2008.severity = error
dotnet_diagnostic.AVP2100.severity = warning
dotnet_diagnostic.AVP2101.severity = warning
# Xaml files
[*.{xaml,axaml}]
indent_size = 2

5
.ncrunch/Avalonia.Controls.ColorPicker.v3.ncrunchproject

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

8
.ncrunch/Avalonia.DBus.Generators.v3.ncrunchproject

@ -0,0 +1,8 @@
<ProjectConfiguration>
<Settings>
<CustomBuildProperties>
<Value>TargetFramework = netstandard2.0</Value>
</CustomBuildProperties>
<IgnoreThisComponentCompletely>False</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.FreeDesktop.AtSpi.v3.ncrunchproject

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

1
Avalonia.Desktop.slnf

@ -55,6 +55,7 @@
"src\\Windows\\Avalonia.Win32.Automation\\Avalonia.Win32.Automation.csproj",
"src\\Windows\\Avalonia.Win32.Interoperability\\Avalonia.Win32.Interoperability.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"tests\\Avalonia.Analyzers.Tests\\Avalonia.Analyzers.Tests.csproj",
"tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Build.Tasks.UnitTest\\Avalonia.Build.Tasks.UnitTest.csproj",

1
Avalonia.slnx

@ -137,6 +137,7 @@
<File Path="build/UnitTests.NetCore.targets" />
</Folder>
<Folder Name="/Tests/">
<Project Path="tests/Avalonia.Analyzers.Tests/Avalonia.Analyzers.Tests.csproj" />
<Project Path="tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj" />
<Project Path="tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj" />
<Project Path="tests/Avalonia.Build.Tasks.UnitTest/Avalonia.Build.Tasks.UnitTest.csproj" />

1
Directory.Build.props

@ -4,6 +4,7 @@
<PropertyGroup>
<PackageOutputPath Condition="'$(PackageOutputPath)' == ''">$(MSBuildThisFileDirectory)build-intermediate/nuget</PackageOutputPath>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\$(AvsCurrentTargetFramework)\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<SharedAssetsPath>$(MSBuildThisFileDirectory)\build\Assets\</SharedAssetsPath>
<!-- https://github.com/dotnet/msbuild/issues/2661 -->
<AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
<RunApiCompat>False</RunApiCompat>

8
Directory.Packages.props

@ -6,7 +6,7 @@
<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.2" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.6" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="Dotnet.Bundle" Version="0.9.13" />
@ -23,6 +23,9 @@
<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.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" 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" />
@ -69,6 +72,7 @@
<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" />
@ -78,4 +82,4 @@
<ItemGroup>
<GlobalPackageReference Include="Microsoft.VisualStudio.SlnGen" Version="8.5.17" PrivateAssets="all" />
</ItemGroup>
</Project>
</Project>

28
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>
@ -145,6 +145,12 @@
<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>
@ -175,6 +181,12 @@
<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>
@ -193,6 +205,12 @@
<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>
@ -223,6 +241,12 @@
<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>
@ -241,4 +265,4 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
</Suppressions>
</Suppressions>

BIN
build/Assets/Icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
build/Assets/icon-32.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
build/Assets/icon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

4
build/Assets/icon.svg

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
<path d="M24 0C37.2548 0 48 10.7452 48 24V42C48 45.3137 45.3137 48 42 48H24C10.7452 48 0 37.2548 0 24C0 10.7452 10.7452 0 24 0ZM24 7C16.3489 7 9.87832 12.0545 7.74496 19.0065C10.3931 19.1342 12.5 21.321 12.5 24C12.5 26.6793 10.3926 28.8664 7.74496 28.9941C9.83517 35.8054 16.0901 40.7958 23.5395 40.9939C26.6226 40.9252 29.1092 40.2605 31 39V41H41V24C41 14.827 33.7348 7.35088 24.6446 7.012L24.323 7.00301L24 7ZM24 16C28.4183 16 32 19.5817 32 24C32 28.4183 28.4183 32 24 32C19.5817 32 16 28.4183 16 24C16 19.5817 19.5817 16 24 16ZM7.5 20.5C5.567 20.5 4 22.067 4 24C4 25.933 5.567 27.5 7.5 27.5C9.433 27.5 11 25.933 11 24C11 22.067 9.433 20.5 7.5 20.5Z" fill="#007DF9"></path>
</svg>

After

Width:  |  Height:  |  Size: 820 B

4
build/SharedVersion.props

@ -10,7 +10,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591;NU5104</NoWarn>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Icon.png</PackageIcon>
<PackageIcon>icon.png</PackageIcon>
<PackageDescription>Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.</PackageDescription>
<PackageTags>avalonia;avaloniaui;mvvm;rx;reactive extensions;android;ios;mac;forms;wpf;net;netstandard;net461;uwp;xamarin</PackageTags>
<PackageReleaseNotes>https://github.com/AvaloniaUI/Avalonia/releases</PackageReleaseNotes>
@ -20,6 +20,6 @@
</PropertyGroup>
<ItemGroup Label="PackageIcon">
<None Include="$(MSBuildThisFileDirectory)/Assets/Icon.png" Pack="true" Visible="false" PackagePath=""/>
<None Include="$(MSBuildThisFileDirectory)/Assets/icon.png" Pack="true" Visible="false" PackagePath=""/>
</ItemGroup>
</Project>

195
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
@ -649,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
@ -661,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
@ -721,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

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

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

@ -465,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];
}
}

8
packages/Avalonia/AvaloniaRules.Project.xml

@ -5,6 +5,7 @@
<Category Name="Compile" DisplayName="Compiling" />
<Category Name="Debug" DisplayName="Debug" />
<Category Name="NameGenerator" DisplayName="Name Generator" />
<Category Name="PropertyGenerator" DisplayName="Property Generator" />
</Rule.Categories>
<Rule.DataSource>
@ -139,4 +140,11 @@ Example: MyApp.Presentation.*, MyApp.Presentation.Views;MyApp.Presentation.Contr
</EnumProperty.Metadata>
</EnumProperty>
<!-- PropertyGenerator -->
<BoolProperty Name="AvaloniaPropertyGeneratorIsEnabled"
DisplayName="Use Property Generator"
Description="Enable/Disable the Avalonia Property Source Generator ([GeneratedStyledProperty]/[GeneratedDirectProperty]/[GeneratedAttachedProperty])"
Category="PropertyGenerator" />
</Rule>

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

4
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -9,8 +9,8 @@
<AndroidPackageFormat>apk</AndroidPackageFormat>
</PropertyGroup>
<ItemGroup>
<AndroidResource Include="..\..\build\Assets\Icon.png">
<Link>Resources\drawable\Icon.png</Link>
<AndroidResource Include="$(SharedAssetsPath)icon.png">
<Link>Resources\drawable\icon.png</Link>
</AndroidResource>
</ItemGroup>

BIN
samples/ControlCatalog.Browser/wwwroot/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 361 KiB

2
samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj

@ -6,6 +6,7 @@
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<UseMicroCom>true</UseMicroCom>
<ApplicationIcon>$(SharedAssetsPath)\icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
@ -35,6 +36,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\MicroCOM.props" />

4
samples/ControlCatalog/App.xaml

@ -74,7 +74,7 @@
</NativeDock.Menu>
<TrayIcon.Icons>
<TrayIcons>
<TrayIcon Icon="/Assets/test_icon.ico" MacOSProperties.IsTemplateIcon="true" ToolTipText="Avalonia Tray Icon ToolTip">
<TrayIcon Icon="/Assets/icon.ico" MacOSProperties.IsTemplateIcon="true" ToolTipText="Avalonia Tray Icon ToolTip">
<TrayIcon.Menu>
<NativeMenu>
<NativeMenuItem Header="Settings">
@ -83,7 +83,7 @@
<NativeMenuItem Header="Option 2" ToggleType="Radio" />
<NativeMenuItemSeparator />
<NativeMenuItem Header="Option 3" ToggleType="CheckBox" IsChecked="True" />
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Restore Defaults" Command="{Binding RestoreDefault}" />
<NativeMenuItem Icon="/Assets/icon.ico" Header="Restore Defaults" Command="{Binding RestoreDefault}" />
<NativeMenuItem Header="Disabled option" IsEnabled="False" />
<NativeMenuItem Header="Hidden option" IsVisible="False" />
</NativeMenu>

BIN
samples/ControlCatalog/Assets/Icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

BIN
samples/ControlCatalog/Assets/avalonia-32.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

BIN
samples/ControlCatalog/Assets/banner-bg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
samples/ControlCatalog/Assets/banner-logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
samples/ControlCatalog/Assets/logo1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
samples/ControlCatalog/Assets/logo2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
samples/ControlCatalog/Assets/logo3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
samples/ControlCatalog/Assets/logo4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
samples/ControlCatalog/Assets/test_icon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

21
samples/ControlCatalog/ControlCatalog.csproj

@ -4,6 +4,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
@ -11,23 +12,13 @@
<AvaloniaXaml Include="**\*.xaml">
<SubType>Designer</SubType>
</AvaloniaXaml>
<AvaloniaResource Include="Assets\**\*" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\Icon.png" />
<None Remove="Assets\image1.jpg" />
<None Remove="Assets\image2.jpg" />
<None Remove="Assets\image3.jpg" />
<None Remove="Assets\image4.jpg" />
<None Remove="Assets\image5.jpg" />
<None Remove="Assets\image6.jpg" />
<None Remove="Assets\image7.jpg" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Bold.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-BoldItalic.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Italic.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Regular.ttf" />
<AvaloniaResource Include="$(SharedAssetsPath)\icon.ico" Link="Assets\icon.ico" />
<AvaloniaResource Include="$(SharedAssetsPath)\icon-32.png" Link="Assets\icon-32.png" />
<AvaloniaResource Include="Assets\**\*" />
<EmbeddedResource Include="Assets\Fonts\*.*" />
<EmbeddedResource Include="Pages\teapot.bin" />
</ItemGroup>

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

3
samples/ControlCatalog/Icons.cs

@ -190,5 +190,8 @@ namespace ControlCatalog
// Slider tune/adjust
public const string Tune = "M3,17V19H9V17H3M3,5V7H13V5H3M13,21V19H21V17H13V15H11V21H13M7,9V11H3V13H7V15H9V9H7M21,13V11H11V13H21M15,9H17V7H21V5H17V3H15V9Z";
// Home
public const string Home = "M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z";
}
}

19
samples/ControlCatalog/MainView.xaml

@ -2,10 +2,8 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ControlCatalog"
xmlns:controls="using:ControlSamples"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:models="using:ControlCatalog.Models"
xmlns:pages="using:ControlCatalog.Pages"
xmlns:viewModels="using:ControlCatalog.ViewModels"
d:DesignWidth="1200"
d:DesignHeight="720"
@ -45,7 +43,7 @@
ItemContainerTheme="{StaticResource NavHeaderItem}"
SelectedIndex="{Binding SelectedPageIndex}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="viewModels:PageItem">
<DataTemplate x:DataType="models:PageItem">
<Grid ColumnDefinitions="20,*">
<PathIcon Grid.Column="0" Width="16" Height="16"
HorizontalAlignment="Center"
@ -77,11 +75,16 @@
Data="M10 2.5a7.5 7.5 0 0 1 5.964 12.048l4.743 4.745a1 1 0 0 1-1.32 1.497l-.094-.083-4.745-4.743A7.5 7.5 0 1 1 10 2.5Zm0 2a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11Z" />
</TextBox.InnerLeftContent>
</TextBox>
<Border HorizontalAlignment="Stretch" Padding="10">
<Image Source="/Assets/Icon.png" VerticalAlignment="Center"
HorizontalAlignment="Center"
MaxHeight="120"
Stretch="Uniform"/>
<Border HorizontalAlignment="Center" Padding="20"
Background="Transparent"
Cursor="Hand"
Tapped="AvaloniaIcon_OnTapped">
<PathIcon VerticalAlignment="Center"
HorizontalAlignment="Center"
Height="100"
Width="100"
Foreground="#007DF9"
Data="M24 0C37.2548 0 48 10.7452 48 24V42C48 45.3137 45.3137 48 42 48H24C10.7452 48 0 37.2548 0 24C0 10.7452 10.7452 0 24 0ZM24 7C16.3489 7 9.87832 12.0545 7.74496 19.0065C10.3931 19.1342 12.5 21.321 12.5 24C12.5 26.6793 10.3926 28.8664 7.74496 28.9941C9.83517 35.8054 16.0901 40.7958 23.5395 40.9939C26.6226 40.9252 29.1092 40.2605 31 39V41H41V24C41 14.827 33.7348 7.35088 24.6446 7.012L24.323 7.00301L24 7ZM24 16C28.4183 16 32 19.5817 32 24C32 28.4183 28.4183 32 24 32C19.5817 32 16 28.4183 16 24C16 19.5817 19.5817 16 24 16ZM7.5 20.5C5.567 20.5 4 22.067 4 24C4 25.933 5.567 27.5 7.5 27.5C9.433 27.5 11 25.933 11 24C11 22.067 9.433 20.5 7.5 20.5Z" />
</Border>
</DockPanel>
</DataTemplate>

6
samples/ControlCatalog/MainView.xaml.cs

@ -2,6 +2,7 @@ using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Styling;
@ -199,5 +200,10 @@ namespace ControlCatalog
ViewModel.SelectedPageIndex = 0;
}
private async void AvaloniaIcon_OnTapped(object? sender, TappedEventArgs e)
{
await TopLevel.GetTopLevel(this)!.Launcher.LaunchUriAsync(new Uri("https://avaloniaui.net/"));
}
}
}

7
samples/ControlCatalog/MainWindow.xaml

@ -1,8 +1,6 @@
<Window xmlns="https://github.com/avaloniaui" MinWidth="500" MinHeight="300"
Width="1024" Height="800"
xmlns:pages="using:ControlCatalog.Pages"
Width="1100" Height="800"
Title="Avalonia Control Gallery"
Icon="/Assets/test_icon.ico"
xmlns:local="using:ControlCatalog"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels;assembly=ControlCatalog"
@ -12,6 +10,7 @@
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">
@ -19,7 +18,7 @@
<NativeMenu>
<NativeMenuItem Header="File">
<NativeMenu>
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Click="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItem Icon="/Assets/icon.ico" Header="Open" Click="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItemSeparator/>
<NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent">
<NativeMenu/>

10
samples/ControlCatalog/Models/HomeSection.cs

@ -0,0 +1,10 @@
using System.Collections.Generic;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Models;
public class HomeSection(string title, IReadOnlyList<PageItem> items)
{
public string Title { get; } = title;
public IReadOnlyList<PageItem> Items { get; } = items;
}

53
samples/ControlCatalog/Models/PageItem.cs

@ -0,0 +1,53 @@
using System;
using System.Globalization;
using System.Text;
using Avalonia.Controls;
using Avalonia.Media;
namespace ControlCatalog.Models;
public class PageItem(string header, Func<Page> factory, StreamGeometry iconData, string description, string section)
{
public string Header { get; } = header;
public StreamGeometry? IconData { get; } = iconData;
public string? Description { get; } = description;
private string SearchKey { get; } = CreateSearchKey(header, section);
public bool IsVisible { get; set; } = true;
public Page CreatePage() => factory();
public bool MatchesSearch(string searchKey)
{
return SearchKey.Contains(searchKey, StringComparison.Ordinal);
}
public static string CreateSearchKey(params ReadOnlySpan<string> values)
{
var builder = new StringBuilder(256);
foreach (var value in values)
{
var normalizedValue = value.Normalize(NormalizationForm.FormKD);
foreach (var c in normalizedValue)
{
var category = CharUnicodeInfo.GetUnicodeCategory(c);
if (category is UnicodeCategory.NonSpacingMark or
UnicodeCategory.SpacingCombiningMark or
UnicodeCategory.EnclosingMark)
{
continue;
}
if (char.IsLetterOrDigit(c))
{
builder.Append(char.ToUpperInvariant(c));
}
}
}
return builder.ToString();
}
}

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>

4
samples/ControlCatalog/Pages/ComboBoxPage.xaml

@ -103,7 +103,7 @@
WrapSelection="{Binding WrapSelection}"
ItemsSource="{Binding Values}"
DisplayMemberBinding="{Binding Name}"
TextSearch.TextBinding="{Binding SearchText, DataType=viewModels:IdAndName}" />
TextSearch.TextBinding="{Binding SearchText, x:DataType=viewModels:IdAndName}" />
<ComboBox
WrapSelection="{Binding WrapSelection}"
@ -139,7 +139,7 @@
<ComboBox WrapSelection="{Binding WrapSelection}" PlaceholderText="Editable"
ItemsSource="{Binding Values}" DisplayMemberBinding="{Binding Name}"
IsEditable="True" Text="{Binding TextValue}"
TextSearch.TextBinding="{Binding SearchText, DataType=viewModels:IdAndName}"
TextSearch.TextBinding="{Binding SearchText, x:DataType=viewModels:IdAndName}"
SelectedItem="{Binding SelectedItem}" />
<TextBlock Text="Editable text is bound to SearchText. Display is bound to Name" />

28
samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs

@ -11,31 +11,25 @@ using Avalonia.Controls.Shapes;
namespace ControlCatalog.Pages
{
public class CustomDrawingExampleControl : Control
public partial class CustomDrawingExampleControl : Control
{
private Point _cursorPoint;
private Geometry _smileGeometry;
public static readonly StyledProperty<double> ScaleProperty = AvaloniaProperty.Register<CustomDrawingExampleControl, double>(nameof(Scale), 1.0d);
public double Scale { get => GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); }
public static readonly StyledProperty<double> RotationProperty = AvaloniaProperty.Register<CustomDrawingExampleControl, double>(nameof(Rotation),
coerce: (_, val) => val % (Math.PI * 2));
[GeneratedStyledProperty(DefaultValue = 1.0d)]
public partial double Scale { get; set; }
/// <summary>
/// Rotation, measured in Radians!
/// </summary>
public double Rotation
{
get => GetValue(RotationProperty);
set => SetValue(RotationProperty, value);
}
[GeneratedStyledProperty(CoerceMethodName = nameof(CoerceRotation))]
public partial double Rotation { get; set; }
public static readonly StyledProperty<double> ViewportCenterYProperty = AvaloniaProperty.Register<CustomDrawingExampleControl, double>(nameof(ViewportCenterY), 0.0d);
public double ViewportCenterY { get => GetValue(ViewportCenterYProperty); set => SetValue(ViewportCenterYProperty, value); }
[GeneratedStyledProperty]
public partial double ViewportCenterY { get; set; }
public static readonly StyledProperty<double> ViewportCenterXProperty = AvaloniaProperty.Register<CustomDrawingExampleControl, double>(nameof(ViewportCenterX), 0.0d);
public double ViewportCenterX { get => GetValue(ViewportCenterXProperty); set => SetValue(ViewportCenterXProperty, value); }
[GeneratedStyledProperty]
public partial double ViewportCenterX { get; set; }
private IPen _pen;
@ -66,7 +60,7 @@ namespace ControlCatalog.Pages
_smileGeometry = sg.Clone();
}
private Geometry _smileGeometry;
private static double CoerceRotation(AvaloniaObject sender, double value) => value % (Math.PI * 2);
protected override void OnPointerMoved(PointerEventArgs e)
{

218
samples/ControlCatalog/Pages/FlexPage.axaml

@ -0,0 +1,218 @@
<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: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;
}
}
}
}
}

152
samples/ControlCatalog/Pages/HomePage.xaml

@ -0,0 +1,152 @@
<ContentPage Theme="{StaticResource ScrollPage}"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="using:ControlCatalog.ViewModels"
xmlns:controlCatalog="using:ControlCatalog"
xmlns:models="clr-namespace:ControlCatalog.Models"
x:Class="ControlCatalog.Pages.HomePage"
Header="Home">
<ContentPage.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="HomeCardBackground" Color="#FFFFFF" />
<SolidColorBrush x:Key="HomeCardBorderBrush" Color="#E5E5EA" />
<SolidColorBrush x:Key="HomeCardBorderBrushHover" Color="#3E6DF3" />
<SolidColorBrush x:Key="HomeSubtleForeground" Color="#64646E" />
<SolidColorBrush x:Key="HomeBadgeBackground" Color="#E8EFFD" />
<SolidColorBrush x:Key="HomeBadgeForeground" Color="#3462D9" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="HomeCardBackground" Color="#2B2B33" />
<SolidColorBrush x:Key="HomeCardBorderBrush" Color="#3A3A45" />
<SolidColorBrush x:Key="HomeCardBorderBrushHover" Color="#8AB4FF" />
<SolidColorBrush x:Key="HomeSubtleForeground" Color="#9C9CAB" />
<SolidColorBrush x:Key="HomeBadgeBackground" Color="#31395A" />
<SolidColorBrush x:Key="HomeBadgeForeground" Color="#8AB4FF" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<ControlTheme x:Key="ButtonCardTheme" TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Background" Value="{DynamicResource HomeCardBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource HomeCardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="16, 14" />
<Setter Property="MinHeight" Value="118" />
<Setter Property="Cursor" Value="Hand" />
<Style Selector="^ /template/ ContentPresenter">
<Setter Property="BoxShadow" Value="0 1 3 0 #0D000000" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="BorderBrush" Duration="0:0:0.1" />
</Transitions>
</Setter>
</Style>
<Style Selector="^:pointerover /template/ ContentPresenter">
<Setter Property="BorderBrush" Value="{DynamicResource HomeCardBorderBrushHover}" />
</Style>
</ControlTheme>
</ResourceDictionary>
</ContentPage.Resources>
<StackPanel Spacing="28" Margin="0,0,15,20" HorizontalAlignment="Stretch"
DataContext="{Binding $parent[controlCatalog:MainView].((viewModels:MainWindowViewModel)DataContext)}">
<!-- Hero banner (1:1 from the Accelerate demo dashboard) -->
<Border CornerRadius="20" ClipToBounds="True" HorizontalAlignment="Stretch" Height="220">
<Grid ClipToBounds="True">
<Image Source="/Assets/banner-bg.png"
Stretch="UniformToFill" />
<Canvas IsHitTestVisible="False">
<Image x:Name="BannerLogo1"
Source="/Assets/logo1.png"
Width="230"
Stretch="Uniform"
Canvas.Left="-60"
Canvas.Bottom="-50" />
<Image x:Name="BannerLogo2"
Source="/Assets/logo2.png"
Width="150"
Stretch="Uniform"
Canvas.Left="240"
Canvas.Top="-60" />
<Image x:Name="BannerLogo3"
Source="/Assets/logo3.png"
Width="210"
Stretch="Uniform"
Canvas.Right="-70"
Canvas.Top="-50" />
<Image x:Name="BannerLogo4"
Source="/Assets/logo4.png"
Width="170"
Stretch="Uniform"
Canvas.Right="40"
Canvas.Bottom="-60" />
</Canvas>
<Image Source="/Assets/banner-logo.png"
Width="220"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</Border>
<!-- Card sections -->
<ItemsControl ItemsSource="{Binding HomeSections}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="models:HomeSection">
<StackPanel Spacing="14" Margin="0,0,0,26">
<TextBlock Text="{Binding Title}"
FontSize="19" FontWeight="Bold" />
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid RowSpacing="12" ColumnSpacing="12"
Margin="0, 0, 1, 0"
SizeChanged="UniformGrid_OnSizeChanged" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="models:PageItem">
<Button Theme="{StaticResource ButtonCardTheme}"
Command="{Binding $parent[controlCatalog:MainView].((viewModels:MainWindowViewModel)DataContext).NavigateToPageCommand}"
CommandParameter="{Binding}">
<StackPanel Spacing="10">
<Border Background="{DynamicResource HomeBadgeBackground}"
CornerRadius="8"
Width="36" Height="36"
HorizontalAlignment="Left">
<PathIcon Data="{Binding IconData}"
Foreground="{DynamicResource HomeBadgeForeground}"
Width="18" Height="18"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Spacing="3">
<TextBlock Text="{Binding Header}"
FontSize="14" FontWeight="SemiBold" />
<TextBlock Text="{Binding Description}"
FontSize="12"
Foreground="{DynamicResource HomeSubtleForeground}"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="2" />
</StackPanel>
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ContentPage>

82
samples/ControlCatalog/Pages/HomePage.xaml.cs

@ -0,0 +1,82 @@
using System;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using Avalonia.Styling;
namespace ControlCatalog.Pages;
public partial class HomePage : ContentPage
{
public HomePage()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
StartFloatingAnimation(BannerLogo1, 12, 8, TimeSpan.FromSeconds(12));
StartFloatingAnimation(BannerLogo2, -14, 8, TimeSpan.FromSeconds(10));
StartFloatingAnimation(BannerLogo3, -12, -12, TimeSpan.FromSeconds(14));
StartFloatingAnimation(BannerLogo4, 12, -8, TimeSpan.FromSeconds(11));
}
private static void StartFloatingAnimation(Control target, double dx, double dy, TimeSpan duration)
{
var transform = new TranslateTransform();
target.RenderTransform = transform;
var animation = new Animation
{
Duration = duration,
IterationCount = IterationCount.Infinite,
Children =
{
new KeyFrame
{
Cue = new Cue(0d),
Setters =
{
new Setter(TranslateTransform.XProperty, 0d),
new Setter(TranslateTransform.YProperty, 0d),
}
},
new KeyFrame
{
Cue = new Cue(0.5d),
Setters =
{
new Setter(TranslateTransform.XProperty, dx),
new Setter(TranslateTransform.YProperty, dy),
}
},
new KeyFrame
{
Cue = new Cue(1d),
Setters =
{
new Setter(TranslateTransform.XProperty, 0d),
new Setter(TranslateTransform.YProperty, 0d),
}
},
}
};
_ = animation.RunAsync(target);
}
private void UniformGrid_OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
if (sender is not UniformGrid grid)
return;
const int minItemWidth = 248;
grid.Columns = Math.Max(
1,
(int)((e.NewSize.Width + grid.ColumnSpacing) / (minItemWidth + grid.ColumnSpacing)));
}
}

1
samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml

@ -7,7 +7,6 @@
<!-- Avatar section -->
<StackPanel HorizontalAlignment="Center" Spacing="8" Margin="16,24,16,16">
<Border Width="70" Height="70"
Background="#2d1b4e"
BorderBrush="#ad2bee" BorderThickness="2"
ClipToBounds="True" HorizontalAlignment="Center">
<Border.Background>

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

1
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

@ -27,6 +27,7 @@
</generic:List>
</ComboBox.ItemsSource>
</ComboBox>
<Button Content="Refresh" Name="RefreshButton"/>
</StackPanel>
</StackPanel>
<RefreshContainer Name="Refresh"

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

233
samples/ControlCatalog/Pages/ScreenPage.cs

@ -1,38 +1,36 @@
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http.Headers;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace ControlCatalog.Pages
{
public class ScreenPage : ContentPage
{
private double _leftMost;
private double _topMost;
private IBrush _primaryBrush = SolidColorBrush.Parse("#FF0078D7");
private IBrush _defaultBrush = Brushes.LightGray;
private IPen _activePen = new Pen(Brushes.Black);
private IPen _defaultPen = new Pen(Brushes.DarkGray);
private readonly ScreensPagePresenter _presenter;
public ScreenPage()
{
var button = new Button();
button.Content = "Request ScreenDetails";
button.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top;
button.SetValue(DockPanel.DockProperty, Dock.Top);
button.Click += async (sender, args) =>
{
var success = TopLevel.GetTopLevel(this)!.Screens is { } screens ?
await screens.RequestScreenDetails() :
false;
var success = TopLevel.GetTopLevel(this)!.Screens is { } screens && await screens.RequestScreenDetails();
button.Content = "Request ScreenDetails: " + (success ? "Granted" : "Denied");
};
Content = button;
_presenter = new ScreensPagePresenter();
Content = new DockPanel
{
Children = { button, _presenter },
VerticalSpacing = 10,
Margin = new Thickness(10)
};
}
protected override bool BypassFlowDirectionPolicies => true;
@ -44,131 +42,140 @@ namespace ControlCatalog.Pages
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel is Window w)
{
w.PositionChanged += (_, _) => InvalidateVisual();
w.PositionChanged += (_, _) => _presenter.InvalidateVisual();
}
if (topLevel?.Screens is { } screens)
{
screens.Changed += (_, _) =>
{
Console.WriteLine("Screens Changed");
InvalidateVisual();
_presenter.InvalidateVisual();
};
}
}
public override void Render(DrawingContext context)
private class ScreensPagePresenter : Control
{
base.Render(context);
double beginOffset = (Content as Visual)?.Bounds.Height + 10 ?? 0;
var topLevel = TopLevel.GetTopLevel(this)!;
if (topLevel.Screens is not { } screens)
private IBrush _primaryBrush = SolidColorBrush.Parse("#FF0078D7");
private IBrush _defaultBrush = Brushes.LightGray;
private IPen _activePen = new Pen(Brushes.Black);
private IPen _defaultPen = new Pen(Brushes.DarkGray);
private double _leftMost;
private double _topMost;
public override void Render(DrawingContext context)
{
var formattedText = CreateFormattedText("Current platform doesn't support Screens API.");
context.DrawText(formattedText, new Point(15, 15 + beginOffset));
return;
}
base.Render(context);
var activeScreen = screens.ScreenFromTopLevel(topLevel);
double maxBottom = 0;
var topLevel = TopLevel.GetTopLevel(this)!;
if (topLevel.Screens is not { } screens)
{
var formattedText = CreateFormattedText("Current platform doesn't support Screens API.");
context.DrawText(formattedText, new Point(15, 15));
return;
}
for (int i = 0; i<screens.ScreenCount; i++ )
{
var screen = screens.All[i];
var activeScreen = screens.ScreenFromTopLevel(topLevel);
double maxBottom = 0;
if (screen.Bounds.X / 10f < _leftMost)
for (int i = 0; i < screens.ScreenCount; i++ )
{
_leftMost = screen.Bounds.X / 10f;
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
return;
var screen = screens.All[i];
if (screen.Bounds.X / 10f < _leftMost)
{
_leftMost = screen.Bounds.X / 10f;
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
return;
}
if (screen.Bounds.Y / 10f < _topMost)
{
_topMost = screen.Bounds.Y / 10f;
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
return;
}
bool primary = screen.IsPrimary;
bool active = screen.Equals(activeScreen);
Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f+Math.Abs(_topMost), screen.Bounds.Width / 10f,
screen.Bounds.Height / 10f);
Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f+Math.Abs(_topMost), screen.WorkingArea.Width / 10f,
screen.WorkingArea.Height / 10f);
context.DrawRectangle(primary ? _primaryBrush : _defaultBrush, active ? _activePen : _defaultPen, boundsRect);
context.DrawRectangle(primary ? _primaryBrush : _defaultBrush, active ? _activePen : _defaultPen, workingAreaRect);
var identifier = CreateScreenIdentifier((i+1).ToString(), primary);
var center = boundsRect.Center - new Point(identifier.Width / 2.0f, identifier.Height / 2.0f);
context.DrawText(identifier, center);
maxBottom = Math.Max(maxBottom, boundsRect.Bottom);
}
if (screen.Bounds.Y / 10f < _topMost)
double currentHeight = maxBottom;
for(int i = 0; i< screens.ScreenCount; i++)
{
_topMost = screen.Bounds.Y / 10f;
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
return;
var screen = screens.All[i];
var formattedText = CreateFormattedText($"Screen {i+1}", 18);
context.DrawText(formattedText, new Point(0, currentHeight));
currentHeight += 25;
formattedText = CreateFormattedText($"DisplayName: {screen.DisplayName}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"Handle: {screen.TryGetPlatformHandle()}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"Scaling: {screen.Scaling * 100}%");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"IsPrimary: {screen.IsPrimary}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"CurrentOrientation: {screen.CurrentOrientation}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText( $"Current: {screen.Equals(activeScreen)}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 30;
}
bool primary = screen.IsPrimary;
bool active = screen.Equals(activeScreen);
Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f+Math.Abs(_topMost) + beginOffset, screen.Bounds.Width / 10f,
screen.Bounds.Height / 10f);
Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f+Math.Abs(_topMost) + beginOffset, screen.WorkingArea.Width / 10f,
screen.WorkingArea.Height / 10f);
context.DrawRectangle(primary ? _primaryBrush : _defaultBrush, active ? _activePen : _defaultPen, boundsRect);
context.DrawRectangle(primary ? _primaryBrush : _defaultBrush, active ? _activePen : _defaultPen, workingAreaRect);
var identifier = CreateScreenIdentifier((i+1).ToString(), primary);
var center = boundsRect.Center - new Point(identifier.Width / 2.0f, identifier.Height / 2.0f + beginOffset);
context.DrawText(identifier, center);
maxBottom = Math.Max(maxBottom, boundsRect.Bottom);
}
double currentHeight = maxBottom;
if (topLevel is Window w)
{
var wPos = w.Position;
var wSize = PixelSize.FromSize(w.FrameSize ?? w.ClientSize, w.DesktopScaling);
context.DrawRectangle(_activePen,
new Rect(wPos.X / 10f + Math.Abs(_leftMost), wPos.Y / 10f + Math.Abs(_topMost),
wSize.Width / 10d, wSize.Height / 10d));
}
}
for(int i = 0; i< screens.ScreenCount; i++)
private static FormattedText CreateFormattedText(string textToFormat, double size = 12)
{
var screen = screens.All[i];
var formattedText = CreateFormattedText($"Screen {i+1}", 18);
context.DrawText(formattedText, new Point(0, currentHeight));
currentHeight += 25;
formattedText = CreateFormattedText($"DisplayName: {screen.DisplayName}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"Handle: {screen.TryGetPlatformHandle()}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"Scaling: {screen.Scaling * 100}%");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"IsPrimary: {screen.IsPrimary}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"CurrentOrientation: {screen.CurrentOrientation}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText( $"Current: {screen.Equals(activeScreen)}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 30;
return new FormattedText(textToFormat, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
Typeface.Default, size, Brushes.Green);
}
if (topLevel is Window w)
private static FormattedText CreateScreenIdentifier(string textToFormat, bool primary)
{
var wPos = w.Position;
var wSize = PixelSize.FromSize(w.FrameSize ?? w.ClientSize, w.DesktopScaling);
context.DrawRectangle(_activePen,
new Rect(wPos.X / 10f + Math.Abs(_leftMost), wPos.Y / 10f + Math.Abs(_topMost) + beginOffset,
wSize.Width / 10d, wSize.Height / 10d));
return new FormattedText(textToFormat, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Typeface.Default, 20, primary ? Brushes.White : Brushes.Black);
}
}
private static FormattedText CreateFormattedText(string textToFormat, double size = 12)
{
return new FormattedText(textToFormat, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
Typeface.Default, size, Brushes.Green);
}
private static FormattedText CreateScreenIdentifier(string textToFormat, bool primary)
{
return new FormattedText(textToFormat, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Typeface.Default, 20, primary ? Brushes.White : Brushes.Black);
}
}
}

75
samples/ControlCatalog/Pages/TabbedPage/FluidNavBar/FluidNavBar.cs

@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
/// The bar background has a bezier "dip" that travels to the selected tab.
/// Each icon is drawn progressively using SKPathMeasure for the fill animation.
/// </summary>
public class FluidNavBar : Control, Avalonia.Rendering.ICustomHitTest
public partial class FluidNavBar : Control, Avalonia.Rendering.ICustomHitTest
{
internal const double NominalHeight = 56.0;
internal const double CircleRadius = 25.0;
@ -34,26 +34,6 @@ namespace ControlCatalog.Pages
internal const double FloatUpDuration = 1.666; // s — circle rising
internal const double FloatDownDuration = 0.833; // s — circle falling
public static readonly StyledProperty<IList<FluidNavItem>> ItemsProperty =
AvaloniaProperty.Register<FluidNavBar, IList<FluidNavItem>>(
nameof(Items), new List<FluidNavItem>());
public static readonly StyledProperty<int> SelectedIndexProperty =
AvaloniaProperty.Register<FluidNavBar, int>(nameof(SelectedIndex), 0);
public static readonly StyledProperty<Color> BarColorProperty =
AvaloniaProperty.Register<FluidNavBar, Color>(nameof(BarColor), Colors.White);
public static readonly StyledProperty<Color> ButtonColorProperty =
AvaloniaProperty.Register<FluidNavBar, Color>(nameof(ButtonColor), Colors.White);
public static readonly StyledProperty<Color> ActiveIconColorProperty =
AvaloniaProperty.Register<FluidNavBar, Color>(nameof(ActiveIconColor), Colors.Black);
public static readonly StyledProperty<Color> InactiveIconColorProperty =
AvaloniaProperty.Register<FluidNavBar, Color>(
nameof(InactiveIconColor), Color.FromArgb(140, 120, 120, 120));
private double _xCurrent = -1; // -1 = not yet initialised
private double _lastWidth = -1; // tracks width changes for resize correction
private double _xStart, _xTarget, _xAnimStartSec;
@ -74,44 +54,35 @@ namespace ControlCatalog.Pages
private readonly Stopwatch _clock = Stopwatch.StartNew();
private bool _animating;
public IList<FluidNavItem> Items
{
get => GetValue(ItemsProperty);
set => SetValue(ItemsProperty, value);
}
[GeneratedStyledProperty]
public partial IReadOnlyList<FluidNavItem> Items { get; set; }
public int SelectedIndex
{
get => GetValue(SelectedIndexProperty);
set => SetValue(SelectedIndexProperty, value);
}
[GeneratedStyledProperty]
public partial int SelectedIndex { get; set; }
public Color BarColor
{
get => GetValue(BarColorProperty);
set => SetValue(BarColorProperty, value);
}
[GeneratedStyledProperty]
public partial Color BarColor { get; set; }
public Color ButtonColor
{
get => GetValue(ButtonColorProperty);
set => SetValue(ButtonColorProperty, value);
}
[GeneratedStyledProperty]
public partial Color ButtonColor { get; set; }
public Color ActiveIconColor
{
get => GetValue(ActiveIconColorProperty);
set => SetValue(ActiveIconColorProperty, value);
}
[GeneratedStyledProperty]
public partial Color ActiveIconColor { get; set; }
public Color InactiveIconColor
{
get => GetValue(InactiveIconColorProperty);
set => SetValue(InactiveIconColorProperty, value);
}
[GeneratedStyledProperty]
public partial Color InactiveIconColor { get; set; }
public event EventHandler<int>? SelectionChanged;
static FluidNavBar()
{
ItemsProperty.OverrideDefaultValue<FluidNavBar>([]);
BarColorProperty.OverrideDefaultValue<FluidNavBar>(Colors.White);
ButtonColorProperty.OverrideDefaultValue<FluidNavBar>(Colors.White);
ActiveIconColorProperty.OverrideDefaultValue<FluidNavBar>(Colors.Black);
InactiveIconColorProperty.OverrideDefaultValue<FluidNavBar>(Color.FromArgb(140, 120, 120, 120));
}
public FluidNavBar()
{
ClipToBounds = false;
@ -604,7 +575,7 @@ namespace ControlCatalog.Pages
while (measure.NextContour());
}
private static float Lerp(float a, float b, float t) => a + (b - a) * t;
private static float Lerp(float a, float b, float t) => float.Lerp(a, b, t);
private static double LinearPoint(double x, double pIn, double pOut)
{

6
samples/ControlCatalog/Pages/Transitions/CardStackPageTransition.cs

@ -424,10 +424,8 @@ public class CardStackPageTransition : PageSlide
return sign * FarPeekAngle;
}
private static double Lerp(double from, double to, double t)
{
return from + ((to - from) * Math.Clamp(t, 0.0, 1.0));
}
private static double Lerp(double from, double to, double t) =>
double.Lerp(from, to, Math.Clamp(t, 0.0, 1.0));
private static int GetViewportZIndex(double offsetFromCenter, Visual visual, Visual? from, Visual? to)
{

59
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}" />
@ -45,7 +75,7 @@
<DockPanel>
<Label Content="Corner Preference (Windows 11+): "
Margin="4"
Margin="0,4"
DockPanel.Dock="Left" />
<ComboBox ItemsSource="{Binding Win32WindowCornerPreferences}"
@ -57,11 +87,12 @@
</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>

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

2
samples/ControlCatalog/ViewModels/CursorPageViewModel.cs

@ -17,7 +17,7 @@ namespace ControlCatalog.ViewModels
.Select(x => new StandardCursorModel(x))
.ToList();
var s = AssetLoader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png"));
var s = AssetLoader.Open(new Uri("avares://ControlCatalog/Assets/icon-32.png"));
var bitmap = new Bitmap(s);
CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16));
}

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

255
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -1,26 +1,25 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Chrome;
using Avalonia.Dialogs;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia;
using Avalonia.Collections;
using ControlCatalog.Models;
using MiniMvvm;
namespace ControlCatalog.ViewModels
{
partial class MainWindowViewModel : ViewModelBase
{
private WindowState _windowState;
private WindowState[] _windowStates = Array.Empty<WindowState>();
private bool _extendClientAreaEnabled;
private double _titleBarHeight;
private bool _isSystemBarVisible;
private bool _displayEdgeToEdge;
private Thickness _safeAreaPadding;
private bool _canResize;
private bool _canMinimize;
private bool _canMaximize;
private int _selectedDecorationIndex;
private readonly AvaloniaList<PageItem> _filteredPages = [];
private bool _ignoreListChange;
private PageItem? _currentItem;
public MainWindowViewModel()
{
@ -37,6 +36,7 @@ namespace ControlCatalog.ViewModels
{
(App.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Shutdown();
});
NavigateToPageCommand = MiniCommand.Create<PageItem>(NavigateToPage);
WindowState = WindowState.Normal;
@ -54,79 +54,177 @@ namespace ControlCatalog.ViewModels
CanMaximize = true;
Filter();
}
}
public IReadOnlyList<HomeSection> HomeSections =>
// Home page doesn't have a section title and should be excluded from this list
field ??= _pageSections.Where(s => !string.IsNullOrEmpty(s.Title)).ToArray();
public IReadOnlyList<PageItem> Pages => _filteredPages;
public INavigation? Navigator { get; internal set; }
public bool ExtendClientAreaEnabled
{
get { return _extendClientAreaEnabled; }
set { RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); }
get;
set => RaiseAndSetIfChanged(ref field, value);
}
public double TitleBarHeight
{
get { return _titleBarHeight; }
set { RaiseAndSetIfChanged(ref _titleBarHeight, value); }
get;
set => RaiseAndSetIfChanged(ref field, value);
}
public WindowState WindowState
{
get { return _windowState; }
set { RaiseAndSetIfChanged(ref _windowState, value); }
get;
set => RaiseAndSetIfChanged(ref field, value);
}
public WindowState[] WindowStates
{
get { return _windowStates; }
set { RaiseAndSetIfChanged(ref _windowStates, value); }
get;
set => RaiseAndSetIfChanged(ref field, value);
}
public bool IsSystemBarVisible
{
get { return _isSystemBarVisible; }
set { RaiseAndSetIfChanged(ref _isSystemBarVisible, value); }
get;
set => RaiseAndSetIfChanged(ref field, value);
}
public bool DisplayEdgeToEdge
{
get { return _displayEdgeToEdge; }
set { RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); }
get;
set => RaiseAndSetIfChanged(ref field, value);
}
public Thickness SafeAreaPadding
{
get { return _safeAreaPadding; }
set { RaiseAndSetIfChanged(ref _safeAreaPadding, value); }
get;
set => RaiseAndSetIfChanged(ref field, value);
}
public bool CanResize
{
get { return _canResize; }
set { RaiseAndSetIfChanged(ref _canResize, value); }
get;
set => RaiseAndSetIfChanged(ref field, value);
}
public bool CanMinimize
{
get { return _canMinimize; }
set { RaiseAndSetIfChanged(ref _canMinimize, value); }
get;
set => RaiseAndSetIfChanged(ref field, value);
}
public bool CanMaximize
{
get { return _canMaximize; }
set { RaiseAndSetIfChanged(ref _canMaximize, value); }
get;
set => RaiseAndSetIfChanged(ref field, value);
}
public int SelectedDecorationIndex
{
get { return _selectedDecorationIndex; }
set { RaiseAndSetIfChanged(ref _selectedDecorationIndex, value); }
get;
set => RaiseAndSetIfChanged(ref field, 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);
}
public int SelectedPageIndex
{
get;
set
{
RaiseAndSetIfChanged(ref field, value);
if (!_ignoreListChange)
{
NavigateTo(field);
if (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay)
IsDrawerOpened = false;
}
}
}
public bool IsDrawerOpened
{
get;
set => RaiseAndSetIfChanged(ref field, value);
} = true;
public SplitViewDisplayMode DisplayMode
{
get;
set => RaiseAndSetIfChanged(ref field, value);
}
public string? Query
{
get;
set
{
RaiseAndSetIfChanged(ref field, value);
Filter(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; }
public MiniCommand ExitCommand { get; }
private DateTime? _validatedDateExample;
public MiniCommand NavigateToPageCommand { get; }
/// <summary>
/// A required DateTime which should demonstrate validation for the DateTimePicker
@ -134,8 +232,8 @@ namespace ControlCatalog.ViewModels
[Required]
public DateTime? ValidatedDateExample
{
get => _validatedDateExample;
set => RaiseAndSetIfChanged(ref _validatedDateExample, value);
get;
set => RaiseAndSetIfChanged(ref field, value);
}
public Win32Properties.WindowCornerPreference[] Win32WindowCornerPreferences { get; } =
@ -144,7 +242,84 @@ namespace ControlCatalog.ViewModels
public Win32Properties.WindowCornerPreference Win32WindowCornerPreference
{
get;
set { RaiseAndSetIfChanged(ref field, value); }
set => RaiseAndSetIfChanged(ref field, value);
}
public void NavigateToPage(PageItem item)
{
// Clear any active search so the target is present in the filtered list.
if (!string.IsNullOrEmpty(Query))
Query = "";
var index = _filteredPages.IndexOf(item);
if (index >= 0)
SelectedPageIndex = index;
}
public void Filter(string? query = "")
{
try
{
_ignoreListChange = true;
_filteredPages.Clear();
// Left panel items are sorted alphabetically
var allPages = _pageSections
.SelectMany(cat => cat.Items)
.OrderBy(p => p.Header == "Home" ? 0 : 1)
.ThenBy(p => p.Header);
if (string.IsNullOrWhiteSpace(query))
{
_filteredPages.AddRange(allPages);
}
else
{
var querySearchKey = PageItem.CreateSearchKey(query);
if (querySearchKey.Length == 0)
{
_filteredPages.AddRange(allPages);
}
else
{
foreach (var item in allPages)
{
if (item.MatchesSearch(querySearchKey))
{
_filteredPages.Add(item);
}
}
}
}
}
finally
{
_ignoreListChange = false;
if (_currentItem != null)
{
var newIndex = _filteredPages.IndexOf(_currentItem);
if (newIndex != -1)
{
SelectedPageIndex = newIndex;
}
}
}
}
private async void NavigateTo(int pageIndex)
{
if (pageIndex < 0 || pageIndex >= Pages.Count || Navigator is null)
return;
var item = Pages[pageIndex];
var page = item.CreatePage();
if (page.GetType() != Navigator.NavigationStack.LastOrDefault()?.GetType())
{
_currentItem = item;
await Navigator.ReplaceAsync(page);
}
}
}
}

354
samples/ControlCatalog/ViewModels/MainWindowViewModel_PageList.cs

@ -1,249 +1,141 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Dialogs;
using System;
using System.ComponentModel.DataAnnotations;
using Avalonia;
using MiniMvvm;
using Avalonia.Collections;
using ControlCatalog.Pages;
using System.Globalization;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using ControlCatalog.Pages;
using Avalonia.Media;
using ControlCatalog.Models;
namespace ControlCatalog.ViewModels
{
partial class MainWindowViewModel
{
private int _selectedPageIndex;
private bool _isDrawerOpened = true;
private bool _ignoreListChange = false;
private string? _query = "";
private PageItem? _currentItem;
private SplitViewDisplayMode _displayMode;
namespace ControlCatalog.ViewModels;
private List<PageItem> _items = new()
partial class MainWindowViewModel
{
private readonly HomeSection[] _pageSections =
[
Section("", s =>
{
new PageItem("Composition", () => new CompositionPage(), Icons.Layers),
new PageItem("Accelerator", () => new AcceleratorPage(), Icons.Keyboard),
new PageItem("Acrylic", () => new AcrylicPage(), Icons.Blur),
new PageItem("AdornerLayer", () => new AdornerLayerPage(), Icons.Sparkle),
new PageItem("AutoCompleteBox",() => new AutoCompleteBoxPage(), Icons.TextInput),
new PageItem("Border",() => new BorderPage(), Icons.Border),
new PageItem("BitmapCache",() => new BitmapCachePage(), Icons.Lightning),
new PageItem("Buttons",() => new ButtonsPage(), Icons.CursorClick),
new PageItem("ButtonSpinner",() => new ButtonSpinnerPage(), Icons.Spinner),
new PageItem("Calendar",() => new CalendarPage(), Icons.Calendar),
new PageItem("Canvas",() => new CanvasPage(), Icons.Canvas),
new PageItem("CommandBar",() => new CommandBarPage(), Icons.Terminal),
new PageItem("Carousel",() => new Pages.CarouselPage(), Icons.Slides),
new PageItem("CarouselPage",() => new CarouselDemoPage(), Icons.Slides),
new PageItem("CheckBox",() => new CheckBoxPage(), Icons.Checkbox),
new PageItem("Clipboard",() => new ClipboardPage(), Icons.Clipboard),
new PageItem("ColorPicker",() => new ColorPickerPage(), Icons.Palette),
new PageItem("ComboBox",() => new ComboBoxPage(), Icons.Dropdown),
new PageItem("Container Queries",() => new ContainerQueryPage(), Icons.Container),
new PageItem("ContentPage",() => new ContentDemoPage(), Icons.Document),
new PageItem("ContextFlyout",() => new ContextFlyoutPage(), Icons.Menu),
new PageItem("ContextMenu",() => new ContextMenuPage(), Icons.Menu),
new PageItem("Cursor",() => new CursorPage(), Icons.Cursor),
new PageItem("Custom Drawing",() => new CustomDrawing(), Icons.Brush),
new PageItem("Data Validation",() => new DataValidationPage(), Icons.Shield),
new PageItem("Date/Time Picker",() => new DateTimePickerPage(), Icons.Clock),
new PageItem("CalendarDatePicker",() => new CalendarDatePickerPage(), Icons.Calendar),
new PageItem("Dialogs",() => new DialogsPage(), Icons.Dialog),
new PageItem("Drag+Drop",() => new DragAndDropPage(), Icons.DragDrop),
new PageItem("DrawerPage",() => new DrawerDemoPage(), Icons.Drawer),
new PageItem("Expander",() => new ExpanderPage(), Icons.Expand),
new PageItem("Flyouts",() => new FlyoutsPage(), Icons.Flyout),
new PageItem("Focus",() => new FocusPage(), Icons.Target),
new PageItem("Gestures",() => new GesturePage(), Icons.Gesture),
new PageItem("Image",() => new ImagePage(), Icons.Image),
new PageItem("Label",() => new LabelsPage(), Icons.Tag),
new PageItem("LayoutTransformControl",() => new LayoutTransformControlPage(), Icons.Transform),
new PageItem("ListBox",() => new ListBoxPage(), Icons.List),
new PageItem("Menu",() => new MenuPage(), Icons.Menu),
new PageItem("NavigationPage",() => new NavigationDemoPage(), Icons.Navigation),
new PageItem("Notifications",() => new NotificationsPage(), Icons.Bell),
new PageItem("NumericUpDown",() => new NumericUpDownPage(), Icons.Number),
new PageItem("OpenGL",() => new OpenGlPage(), Icons.Cube3D),
new PageItem("OpenGL Lease",() => new OpenGlLeasePage(), Icons.Cube3D),
new PageItem("PipsPager",() => new PipsPagerPage(), Icons.HorizontalDots),
new PageItem("Platform Information",() => new PlatformInfoPage(), Icons.Info),
new PageItem("Pointers",() => new PointersPage(), Icons.Cursor),
new PageItem("ProgressBar",() => new ProgressBarPage(), Icons.Progress),
new PageItem("RadioButton",() => new RadioButtonPage(), Icons.Radio),
new PageItem("RefreshContainer",() => new RefreshContainerPage(), Icons.Refresh),
new PageItem("RelativePanel",() => new RelativePanelPage(), Icons.Layout),
new PageItem("ScrollViewer",() => new ScrollViewerPage(), Icons.Scroll),
new PageItem("Slider",() => new SliderPage(), Icons.Tune),
new PageItem("SplitView",() => new SplitViewPage(), Icons.Split),
new PageItem("TabbedPage",() => new TabbedDemoPage(), Icons.Tab),
new PageItem("TabControl",() => new TabControlPage(), Icons.Tab),
new PageItem("TabStrip",() => new TabStripPage(), Icons.Tab),
new PageItem("TableView",() => new TableViewPage(), Icons.Grid),
new PageItem("TextBox",() => new TextBoxPage(), Icons.TextInput),
new PageItem("TextBlock",() => new TextBlockPage(), Icons.TextInput),
new PageItem("Theme Variants",() => new ThemePage(), Icons.Theme),
new PageItem("ToggleSwitch",() => new ToggleSwitchPage(), Icons.Toggle),
new PageItem("ToolTip",() => new ToolTipPage(), Icons.Tooltip),
new PageItem("TransitioningContentControl",() => new TransitioningContentControlPage(), Icons.Transition),
new PageItem("TreeView",() => new TreeViewPage(), Icons.Tree),
new PageItem("Viewbox",() => new ViewboxPage(), Icons.Viewbox),
new PageItem("WrapPanel",() => new WrapPanelPage(), Icons.Layout),
new PageItem("Native Embed",() => new NativeEmbedPage(), Icons.Puzzle),
new PageItem("Window Customizations",() => new WindowCustomizationsPage(), Icons.Window),
new PageItem("HeaderedContentControl",() => new HeaderedContentPage(), Icons.Header),
new PageItem("Screens",() => new ScreenPage(), Icons.Monitor),
};
public AvaloniaList<PageItem> Pages { get; } = new AvaloniaList<PageItem>();
public void Filter(string? query = "")
s.Add<HomePage>("Home", Icons.Home, "Overview of everything in the catalog");
}),
Section("Basic Input", s =>
{
try
{
_ignoreListChange = true;
Pages.Clear();
if (string.IsNullOrWhiteSpace(query))
{
Pages.AddRange(_items);
}
else
{
var querySearchKey = PageItem.CreateSearchKey(query);
if (querySearchKey.Length == 0)
{
Pages.AddRange(_items);
}
else
{
foreach (var item in _items)
{
if (item.MatchesSearch(querySearchKey))
{
Pages.Add(item);
}
}
}
}
}
finally
{
_ignoreListChange = false;
if (_currentItem != null)
{
var newIndex = Pages.IndexOf(_currentItem);
if (newIndex != -1)
{
SelectedPageIndex = newIndex;
}
}
}
}
public INavigation? Navigator { get; internal set; }
public int SelectedPageIndex
s.Add<ButtonsPage>("Buttons", Icons.CursorClick, "Button, RepeatButton, ToggleButton and friends");
s.Add<ButtonSpinnerPage>("ButtonSpinner", Icons.Spinner, "Content with increment and decrement buttons");
s.Add<CheckBoxPage>("CheckBox", Icons.Checkbox, "Two- and three-state check boxes");
s.Add<ColorPickerPage>("ColorPicker", Icons.Palette, "Pick colors from spectrum and palette views");
s.Add<ComboBoxPage>("ComboBox", Icons.Dropdown, "A drop-down list of selectable items");
s.Add<NumericUpDownPage>("NumericUpDown", Icons.Number, "Numeric input with spinner buttons");
s.Add<RadioButtonPage>("RadioButton", Icons.Radio, "Mutually exclusive option groups");
s.Add<SliderPage>("Slider", Icons.Tune, "Select a value from a continuous range");
s.Add<ToggleSwitchPage>("ToggleSwitch", Icons.Toggle, "An on/off switch with a sliding knob");
}),
Section("Text", s =>
{
get { return _selectedPageIndex; }
set
{
RaiseAndSetIfChanged(ref _selectedPageIndex, value);
if (!_ignoreListChange)
{
NavigateTo(_selectedPageIndex);
if (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay)
IsDrawerOpened = false;
}
}
}
public bool IsDrawerOpened
s.Add<AutoCompleteBoxPage>("AutoCompleteBox", Icons.TextInput, "Text input with completion suggestions");
s.Add<LabelsPage>("Label", Icons.Tag, "Captions with access keys for other controls");
s.Add<TextBoxPage>("TextBox", Icons.TextInput, "Single- and multi-line text editing");
s.Add<TextBlockPage>("TextBlock", Icons.TextInput, "Styled read-only text display");
}),
Section("Collections & Data", s =>
{
get { return _isDrawerOpened; }
set { RaiseAndSetIfChanged(ref _isDrawerOpened, value); }
}
public SplitViewDisplayMode DisplayMode
s.Add<Pages.CarouselPage>("Carousel", Icons.Slides, "Cycle through a collection of items");
s.Add<ListBoxPage>("ListBox", Icons.List, "A selectable, virtualized list of items");
s.Add<PipsPagerPage>("PipsPager", Icons.HorizontalDots, "Dot-style pager for paginated content");
s.Add<RefreshContainerPage>("RefreshContainer", Icons.Refresh, "Pull-to-refresh for scrollable content");
s.Add<TableViewPage>("TableView", Icons.Grid, "Tabular data with resizable, sortable columns");
s.Add<TreeViewPage>("TreeView", Icons.Tree, "Hierarchical data with expandable nodes");
}),
Section("Date & Time", s =>
{
get { return _displayMode; }
set { RaiseAndSetIfChanged(ref _displayMode, value); }
}
public string? Query
s.Add<CalendarPage>("Calendar", Icons.Calendar, "A month calendar for selecting dates");
s.Add<CalendarDatePickerPage>("CalendarDatePicker", Icons.Calendar, "A date picker with a drop-down calendar");
s.Add<DateTimePickerPage>("Date/Time Picker", Icons.Clock, "Spinner-style date and time pickers");
}),
Section("Menus & Flyouts", s =>
{
get { return _query; }
set
{
RaiseAndSetIfChanged(ref _query, value);
Filter(value);
}
}
private async void NavigateTo(int pageIndex)
s.Add<CommandBarPage>("CommandBar", Icons.Terminal, "A toolbar of commands with an overflow menu");
s.Add<ContextFlyoutPage>("ContextFlyout", Icons.Menu, "Attach flyouts shown on right-click");
s.Add<ContextMenuPage>("ContextMenu", Icons.Menu, "Traditional right-click context menus");
s.Add<FlyoutsPage>("Flyouts", Icons.Flyout, "Lightweight popups anchored to controls");
s.Add<MenuPage>("Menu", Icons.Menu, "Menu bars with nested menu items");
}),
Section("Navigation & Pages", s =>
{
if (pageIndex < 0 || pageIndex >= Pages.Count || Navigator is null)
return;
var item = Pages[pageIndex];
if (item != null)
{
var view = item.Factory();
if (view is Page page && view.GetType() != Navigator.NavigationStack.LastOrDefault()?.GetType())
{
_currentItem = item;
await Navigator.ReplaceAsync(page);
}
}
}
s.Add<CarouselDemoPage>("CarouselPage", Icons.Slides, "Swipeable page-based navigation");
s.Add<ContentDemoPage>("ContentPage", Icons.Document, "A page that hosts a single content view");
s.Add<DrawerDemoPage>("DrawerPage", Icons.Drawer, "A page with a sliding navigation drawer");
s.Add<NavigationDemoPage>("NavigationPage", Icons.Navigation, "Stack-based page navigation");
s.Add<SplitViewPage>("SplitView", Icons.Split, "A collapsible pane beside content");
s.Add<TabbedDemoPage>("TabbedPage", Icons.Tab, "Tab-based page navigation");
s.Add<TabControlPage>("TabControl", Icons.Tab, "Switch between tabbed content views");
s.Add<TabStripPage>("TabStrip", Icons.Tab, "A standalone strip of selectable tabs");
}),
Section("Layout", s =>
{
s.Add<BorderPage>("Border", Icons.Border, "Decorate elements with borders and corner radii");
s.Add<CanvasPage>("Canvas", Icons.Canvas, "Position children at explicit coordinates");
s.Add<ContainerQueryPage>("Container Queries", Icons.Container, "Styles that respond to container size");
s.Add<ExpanderPage>("Expander", Icons.Expand, "A header that expands to reveal content");
s.Add<FlexPage>("Flex Panel", Icons.Grid, "Flexible, CSS-style child layout");
s.Add<HeaderedContentPage>("HeaderedContentControl", Icons.Header, "Content paired with a header");
s.Add<LayoutTransformControlPage>("LayoutTransformControl", Icons.Transform, "Apply transforms that affect layout");
s.Add<RelativePanelPage>("RelativePanel", Icons.Layout, "Arrange children relative to each other");
s.Add<ScrollViewerPage>("ScrollViewer", Icons.Scroll, "Scrollable viewport over large content");
s.Add<ViewboxPage>("Viewbox", Icons.Viewbox, "Scale content to fit available space");
s.Add<WrapPanelPage>("WrapPanel", Icons.Layout, "Wrap children onto multiple lines");
}),
Section("Media & Graphics", s =>
{
s.Add<AcrylicPage>("Acrylic", Icons.Blur, "Translucent acrylic window materials");
s.Add<BitmapCachePage>("BitmapCache", Icons.Lightning, "Cache visuals as bitmaps for performance");
s.Add<CompositionPage>("Composition", Icons.Layers, "Composition-layer animations and effects");
s.Add<CustomDrawing>("Custom Drawing", Icons.Brush, "Render custom geometry in code");
s.Add<ImagePage>("Image", Icons.Image, "Display bitmaps with different stretch modes");
s.Add<OpenGlPage>("OpenGL", Icons.Cube3D, "Embed custom OpenGL rendering");
s.Add<OpenGlLeasePage>("OpenGL Lease", Icons.Cube3D, "Low-level access to the OpenGL context");
s.Add<TransitioningContentControlPage>("TransitioningContentControl", Icons.Transition, "Animate between content changes");
}),
Section("Status & Feedback", s =>
{
s.Add<AdornerLayerPage>("AdornerLayer", Icons.Sparkle, "Overlay visuals on top of other controls");
s.Add<DataValidationPage>("Data Validation", Icons.Shield, "Display validation errors from bindings");
s.Add<DialogsPage>("Dialogs", Icons.Dialog, "File pickers and modal dialog windows");
s.Add<NotificationsPage>("Notifications", Icons.Bell, "Toast-style in-app notifications");
s.Add<ProgressBarPage>("ProgressBar", Icons.Progress, "Determinate and indeterminate progress");
s.Add<ToolTipPage>("ToolTip", Icons.Tooltip, "Hover hints for any control");
}),
Section("Interaction", s =>
{
s.Add<AcceleratorPage>("Accelerator", Icons.Keyboard, "Keyboard shortcuts that invoke commands");
s.Add<ClipboardPage>("Clipboard", Icons.Clipboard, "Read from and write to the system clipboard");
s.Add<CursorPage>("Cursor", Icons.Cursor, "Change the pointer cursor over elements");
s.Add<DragAndDropPage>("Drag+Drop", Icons.DragDrop, "Drag data within and between applications");
s.Add<FocusPage>("Focus", Icons.Target, "Track and control keyboard focus");
s.Add<GesturePage>("Gestures", Icons.Gesture, "Tap, scroll and pinch gesture recognition");
s.Add<PointersPage>("Pointers", Icons.Cursor, "Raw pointer input and capture");
}),
Section("Window & Platform", s =>
{
s.Add<NativeEmbedPage>("Native Embed", Icons.Puzzle, "Host native platform controls");
s.Add<PlatformInfoPage>("Platform Information", Icons.Info, "Runtime platform and capability info");
s.Add<PlatformSettingsPage>("Platform Settings", Icons.Tune, "Platform-specific system settings");
s.Add<ScreenPage>("Screens", Icons.Monitor, "Enumerate displays and their bounds");
s.Add<ThemePage>("Theme Variants", Icons.Theme, "Switch between light and dark variants");
s.Add<WindowCustomizationsPage>("Window Customizations", Icons.Window, "Custom chrome, decorations and sizing");
})
];
private static HomeSection Section(string title, Action<HomeSectionBuilder> builderCallback)
{
var builder = new HomeSectionBuilder(title);
builderCallback(builder);
return new HomeSection(title, builder.ToArray());
}
class PageItem(string header, Func<object> factory, string? iconData = null)
private class HomeSectionBuilder(string title) : List<PageItem>
{
public string Header { get; } = header;
public Func<object> Factory { get; } = factory;
public string? IconData { get; } = iconData;
private string SearchKey { get; } = CreateSearchKey(header);
public bool IsVisible { get; set; } = true;
public bool MatchesSearch(string searchKey)
{
return SearchKey.Contains(searchKey, StringComparison.Ordinal);
}
public static string CreateSearchKey(string value)
public void Add<TPageType>(string header, string iconPath, string description) where TPageType : Page, new()
{
var normalizedValue = value.Normalize(NormalizationForm.FormKD);
var builder = new StringBuilder(normalizedValue.Length);
foreach (var c in normalizedValue)
{
var category = CharUnicodeInfo.GetUnicodeCategory(c);
if (category is UnicodeCategory.NonSpacingMark or
UnicodeCategory.SpacingCombiningMark or
UnicodeCategory.EnclosingMark)
{
continue;
}
if (char.IsLetterOrDigit(c))
{
builder.Append(char.ToUpperInvariant(c));
}
}
return builder.ToString();
var iconGeometry = StreamGeometry.Parse(iconPath);
Add(new PageItem(header, () => new TPageType(), iconGeometry, description, title));
}
}
}

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

5
samples/Directory.Build.props

@ -1,5 +1,7 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)\..\Directory.Build.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\$(AvsCurrentTargetFramework)\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
@ -9,12 +11,11 @@
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);CS8002</NoWarn> <!-- ignore signing warnings for samples -->
<EnableRuntimeMarshalling>true</EnableRuntimeMarshalling>
<ApplicationIcon>$(SharedAssetsPath)icon.ico</ApplicationIcon> <!-- defualt icon shared between projects -->
</PropertyGroup>
<Import Project="..\build\SharedVersion.props"/>
<Import Project="..\build\DevAnalyzers.props"/>
<Import Project="..\build\TrimmingEnable.props" />
<Import Project="$(MSBuildThisFileDirectory)\..\Directory.Build.props" />
</Project>

BIN
samples/Generators.Sandbox/Assets/Icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

BIN
samples/IntegrationTestApp/Assets/icon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

1
samples/IntegrationTestApp/IntegrationTestApp.csproj

@ -15,6 +15,7 @@
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="$(SharedAssetsPath)\icon.ico" Link="Assets\icon.ico" />
<AvaloniaResource Include="Assets\*" />
</ItemGroup>

1
samples/IntegrationTestApp/MainWindow.axaml

@ -5,7 +5,6 @@
xmlns:vm="using:IntegrationTestApp.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="IntegrationTestApp.MainWindow"
Icon="/Assets/icon.ico"
Title="IntegrationTestApp"
x:DataType="vm:MainWindowViewModel">
<NativeMenu.Menu>

1
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -86,6 +86,7 @@ namespace IntegrationTestApp
new("DragDrop", () => new DragDropPage()),
new("Embedding", () => new EmbeddingPage()),
new("Gestures", () => new GesturesPage()),
new("Keyboard", () => new KeyboardPage()),
new("ListBox", () => new ListBoxPage()),
new("Menu", () => new MenuPage()),
new("Pointer", () => new PointerPage()),

45
samples/IntegrationTestApp/Pages/KeyboardPage.axaml

@ -0,0 +1,45 @@
<UserControl 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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="IntegrationTestApp.Pages.KeyboardPage">
<StackPanel Margin="8" Spacing="8">
<Border Name="GestureScope" Padding="8" BorderThickness="1" BorderBrush="Gray">
<StackPanel Spacing="4">
<TextBlock>Key bindings in scope: Space, A, Ctrl+G</TextBlock>
<TextBox Name="GestureTextBox"/>
</StackPanel>
</Border>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock>Gesture text box content:</TextBlock>
<TextBlock Name="GestureTextBoxContent" Text="{Binding #GestureTextBox.Text}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock>Last key binding:</TextBlock>
<TextBlock Name="LastKeyBinding"/>
</StackPanel>
<TextBlock Margin="0,8,0,0">No key bindings in scope, used to inspect raw key downs:</TextBlock>
<TextBox Name="KeyDownTextBox"/>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock>Last key down:</TextBlock>
<TextBlock Name="LastKeyDown"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock>Key down count:</TextBlock>
<TextBlock Name="KeyDownCount"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock>Last text input:</TextBlock>
<TextBlock Name="LastTextInput"/>
</StackPanel>
<Button Name="ResetKeyboard" Click="ResetKeyboard_Click" HorizontalAlignment="Left">Reset</Button>
</StackPanel>
</UserControl>

63
samples/IntegrationTestApp/Pages/KeyboardPage.axaml.cs

@ -0,0 +1,63 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace IntegrationTestApp.Pages;
public partial class KeyboardPage : UserControl
{
private int _keyDownCount;
public KeyboardPage()
{
InitializeComponent();
// Gestures without a modifier are the interesting case: on macOS the key down used to be
// swallowed by the input context while a text input client was active.
AddKeyBinding(new KeyGesture(Key.Space), "Space");
AddKeyBinding(new KeyGesture(Key.A), "A");
AddKeyBinding(new KeyGesture(Key.G, KeyModifiers.Control), "Ctrl+G");
// TextBox handles TextInput, and KeyDown for keys like Backspace and the arrows, in a
// class handler that runs before instance handlers. Subscribe with handledEventsToo so
// those keys stay visible here.
KeyDownTextBox.AddHandler(KeyDownEvent, KeyDownTextBox_KeyDown, handledEventsToo: true);
KeyDownTextBox.AddHandler(TextInputEvent, KeyDownTextBox_TextInput, handledEventsToo: true);
}
private void AddKeyBinding(KeyGesture gesture, string name)
{
GestureScope.KeyBindings.Add(new KeyBinding
{
Gesture = gesture,
Command = new DelegateCommand(() => LastKeyBinding.Text = name)
});
}
private void KeyDownTextBox_KeyDown(object? sender, KeyEventArgs e)
{
// While an input method is composing, the key is masked as Key.ImeProcessed but the
// physical key and the key symbol keep their real values. The key symbol is bracketed
// to keep whitespace symbols like the space key's visible and assertable.
LastKeyDown.Text = $"{e.Key}|{e.PhysicalKey}|[{e.KeySymbol}]";
// Counts every key down, including the ones flagsChanged raises for modifier keys.
KeyDownCount.Text = (++_keyDownCount).ToString();
}
private void KeyDownTextBox_TextInput(object? sender, TextInputEventArgs e)
{
LastTextInput.Text = $"[{e.Text}]";
}
private void ResetKeyboard_Click(object? sender, RoutedEventArgs e)
{
_keyDownCount = 0;
KeyDownCount.Text = string.Empty;
LastKeyBinding.Text = string.Empty;
LastKeyDown.Text = string.Empty;
LastTextInput.Text = string.Empty;
GestureTextBox.Text = string.Empty;
KeyDownTextBox.Text = string.Empty;
}
}

4
samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj

@ -16,8 +16,8 @@
</ItemGroup>
<ItemGroup>
<AndroidResource Include="..\..\build\Assets\Icon.png">
<Link>Resources\drawable\Icon.png</Link>
<AndroidResource Include="$(SharedAssetsPath)icon.png">
<Link>Resources\drawable\icon.png</Link>
</AndroidResource>
</ItemGroup>

1
samples/SafeAreaDemo.Desktop/SafeAreaDemo.Desktop.csproj

@ -15,5 +15,6 @@
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" />
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
</Project>

BIN
samples/SafeAreaDemo/Assets/avalonia-logo.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

129
samples/SafeAreaDemo/Views/MainView.xaml

@ -9,64 +9,77 @@
x:Class="SafeAreaDemo.Views.MainView"
x:DataType="vm:MainViewModel"
TopLevel.AutoSafeAreaPadding="{Binding AutoSafeAreaPadding, Mode=TwoWay}">
<Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Bounds="{Binding CanvasSize, Mode=OneWayToSource}">
<Border BorderBrush="Red"
Margin="{Binding ViewPadding}"
BorderThickness="1">
<Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Bounds="{Binding CanvasSize, Mode=OneWayToSource}">
<Border BorderBrush="Red"
Margin="{Binding ViewPadding}"
BorderThickness="1">
<Grid>
<Label Margin="5"
Foreground="Red"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Right">View Bounds</Label>
<Label Margin="5"
Foreground="Red"
VerticalAlignment="Bottom"
HorizontalContentAlignment="Right">View Bounds</Label>
</Grid>
</Border>
<Border BorderBrush="LimeGreen"
Margin="{Binding SafeAreaPadding}"
BorderThickness="1">
<Grid>
<Label Margin="5"
Foreground="LimeGreen"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" >Safe Area</Label>
<InputPaneAwareDecorator Behavior="Pan">
<Border BorderBrush="Blue"
BorderThickness="1">
<Grid>
<Label Margin="5"
Foreground="Red"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Right">View Bounds</Label>
<Label Margin="5"
Foreground="Red"
VerticalAlignment="Bottom"
HorizontalContentAlignment="Right">View Bounds</Label>
<Label Margin="5"
Foreground="Blue"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
HorizontalContentAlignment="Center" >Input Pane Aware Area</Label>
<Grid DockPanel.Dock="Bottom"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<StackPanel Orientation="Vertical"
Spacing="5"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Label HorizontalAlignment="Left">Options:</Label>
<CheckBox IsChecked="{Binding DisplayEdgeToEdge}">Display Edge To Edge</CheckBox>
<CheckBox IsChecked="{Binding UseSafeArea}" IsEnabled="{Binding !AutoSafeAreaPadding}">Use Safe Area</CheckBox>
<CheckBox IsChecked="{Binding AutoSafeAreaPadding}">Automatic Paddings</CheckBox>
<CheckBox IsChecked="{Binding HideSystemBars}">Hide System Bars</CheckBox>
<TextBox Width="200" PlaceholderText="Tap to Show Keyboard"/>
<StackPanel Orientation="Horizontal" Spacing="10">
<Button Command="{Binding SetLight}">Light</Button>
<Button Command="{Binding SetDark}">Dark</Button>
<Button Command="{Binding SetDefault}">Default</Button>
</StackPanel>
</StackPanel>
</Grid>
</Grid>
</Border>
<Border BorderBrush="LimeGreen"
Margin="{Binding SafeAreaPadding}"
BorderThickness="1">
<DockPanel>
<Label Margin="5"
Foreground="LimeGreen"
DockPanel.Dock="Bottom"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" >Safe Area</Label>
<Grid DockPanel.Dock="Bottom"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<StackPanel Orientation="Vertical"
Spacing="5"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Label HorizontalAlignment="Left">Options:</Label>
<CheckBox IsChecked="{Binding DisplayEdgeToEdge}">Display Edge To Edge</CheckBox>
<CheckBox IsChecked="{Binding UseSafeArea}" IsEnabled="{Binding !AutoSafeAreaPadding}">Use Safe Area</CheckBox>
<CheckBox IsChecked="{Binding AutoSafeAreaPadding}">Automatic Paddings</CheckBox>
<CheckBox IsChecked="{Binding HideSystemBars}">Hide System Bars</CheckBox>
<TextBox Width="200" PlaceholderText="Tap to Show Keyboard"/>
<StackPanel Orientation="Horizontal" Spacing="10">
<Button Command="{Binding SetLight}">Light</Button>
<Button Command="{Binding SetDark}">Dark</Button>
<Button Command="{Binding SetDefault}">Default</Button>
</StackPanel>
</StackPanel>
</Grid>
</DockPanel>
</Border>
<Button Margin="{Binding InputPaneMarkerMargin}"
VerticalAlignment="Bottom"
Content="X">
<Button.Transitions>
<Transitions>
<ThicknessTransition Property="Margin"
Duration="{Binding InputPaneDuration}"
Easing="{Binding InputPaneEasing}"/>
</Transitions>
</Button.Transitions>
</Button>
</Grid>
</Border>
</InputPaneAwareDecorator>
</Grid>
</Border>
<Button Margin="{Binding InputPaneMarkerMargin}"
VerticalAlignment="Bottom"
Content="X">
<Button.Transitions>
<Transitions>
<ThicknessTransition Property="Margin"
Duration="{Binding InputPaneDuration}"
Easing="{Binding InputPaneEasing}"/>
</Transitions>
</Button.Transitions>
</Button>
</Grid>
</UserControl>

1
samples/SafeAreaDemo/Views/MainWindow.xaml

@ -6,7 +6,6 @@
xmlns:views="clr-namespace:SafeAreaDemo.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SafeAreaDemo.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="SafeAreaDemo">
<views:MainView />
</Window>

BIN
samples/SingleProjectSandbox/Platforms/Browser/wwwroot/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 361 KiB

1
src/Android/Avalonia.Android/Avalonia.Android.csproj

@ -12,6 +12,7 @@
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<PackageReference Include="Xamarin.AndroidX.AppCompat" />
<PackageReference Include="Xamarin.AndroidX.Window" />
<PackageReference Include="Xamarin.AndroidX.DocumentFile" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />

45
src/Android/Avalonia.Android/AvaloniaAccessHelper.cs

@ -49,6 +49,14 @@ namespace Avalonia.Android
private readonly Dictionary<AutomationPeer, HashSet<INodeInfoProvider>> _peerNodeInfoProviders;
/// <remarks>
/// Virtual view IDs must be allocated from a monotonic counter rather than derived from
/// the size of <see cref="_peerNodeInfoProviders"/>: entries are now removed when their
/// owner leaves the visual tree, so the dictionary's count no longer grows monotonically
/// and reusing it would hand out an ID that is still in use.
/// </remarks>
private int _nextPeerViewId;
private readonly AvaloniaView _view;
public AvaloniaAccessHelper(AvaloniaView view) : base(view)
@ -85,16 +93,16 @@ namespace Avalonia.Android
}
else
{
peerViewId = _peerNodeInfoProviders.Count;
peerViewId = _nextPeerViewId++;
_peers.Add(peerViewId, peer);
_peerIds.Add(peer, peerViewId);
nodeInfoProviders = new();
_peerNodeInfoProviders.Add(peer, nodeInfoProviders);
peer.ChildrenChanged += (s, ev) => InvalidateVirtualView(peerViewId,
EventHandler childrenChanged = (s, ev) => InvalidateVirtualView(peerViewId,
AccessibilityEventCompat.ContentChangeTypeSubtree);
peer.PropertyChanged += (s, ev) =>
EventHandler<AutomationPropertyChangedEventArgs> propertyChanged = (s, ev) =>
{
if (ev.Property == AutomationElementIdentifiers.NameProperty)
{
@ -104,13 +112,42 @@ namespace Avalonia.Android
{
InvalidateVirtualView(peerViewId, AccessibilityEventCompat.ContentChangeTypeContentDescription);
}
else if (ev.Property == AutomationElementIdentifiers.BoundingRectangleProperty ||
else if (ev.Property == AutomationElementIdentifiers.BoundingRectangleProperty ||
ev.Property == AutomationElementIdentifiers.ClassNameProperty)
{
InvalidateVirtualView(peerViewId);
}
};
peer.ChildrenChanged += childrenChanged;
peer.PropertyChanged += propertyChanged;
// Drop the registration once the peer's control leaves the visual tree, otherwise
// every control ever explored by accessibility is kept alive for the lifetime of
// the view: the peer holds a strong reference to its Owner, and these three
// dictionaries were never pruned. On a long-running app that rebuilds its UI (for
// instance digital signage swapping screens), this retains each dead visual tree in
// full — measured at ~2.5 MB per rebuild on a real device.
// The root peer (ID 0) is deliberately never unregistered: GetVirtualViewAt and
// GetVisibleVirtualViews index _peers[0] directly, so removing it would throw.
// It is a single entry owned by the view itself, and dies with the helper.
if (peerViewId != 0 && peer is ControlAutomationPeer controlPeer)
{
EventHandler<VisualTreeAttachmentEventArgs>? detachedFromVisualTree = null;
detachedFromVisualTree = (s, ev) =>
{
controlPeer.Owner.DetachedFromVisualTree -= detachedFromVisualTree;
peer.ChildrenChanged -= childrenChanged;
peer.PropertyChanged -= propertyChanged;
_peers.Remove(peerViewId);
_peerIds.Remove(peer);
_peerNodeInfoProviders.Remove(peer);
};
controlPeer.Owner.DetachedFromVisualTree += detachedFromVisualTree;
}
if (peer.GetProvider<IExpandCollapseProvider>() is not null)
nodeInfoProviders.Add(new ExpandCollapseNodeInfoProvider(this, peer, peerViewId));
if (peer.GetProvider<IInvokeProvider>() is not null)

20
src/Android/Avalonia.Android/AvaloniaView.cs

@ -23,7 +23,7 @@ namespace Avalonia.Android
private readonly ViewImpl _view;
private readonly ExploreByTouchHelper _accessHelper;
private IDisposable? _timerSubscription;
private bool _isRendering;
private bool _surfaceCreated;
public AvaloniaView(Context context) : base(context)
@ -106,13 +106,9 @@ namespace Avalonia.Android
if (_root == null || !_surfaceCreated)
return;
if (isVisible && _timerSubscription == null)
if (isVisible && !_isRendering)
{
if (AndroidPlatform.Timer is { } timer)
{
_timerSubscription = timer.SubscribeView(this);
}
_isRendering = true;
_root.StartRendering();
if (_view.TryGetFeature<IInsetsManager>(out var insetsManager) == true)
@ -120,11 +116,10 @@ namespace Avalonia.Android
(insetsManager as AndroidInsetsManager)?.ApplyStatusBarState();
}
}
else if (!isVisible && _timerSubscription != null)
else if (!isVisible && _isRendering)
{
_isRendering = false;
_root.StopRendering();
_timerSubscription?.Dispose();
_timerSubscription = null;
}
}
@ -137,11 +132,8 @@ namespace Avalonia.Android
private void SendConfigurationChanged(Configuration? newConfig)
{
_view.InsetsManager?.SetDefaultSystemLightMode(!(newConfig?.UiMode.HasFlag(UiMode.NightYes) ?? false));
if (Context is { } context && newConfig is { } config)
if (Context is not null && newConfig is not null)
{
var settings =
AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>() as AndroidPlatformSettings;
settings?.OnViewConfigurationChanged(context, config);
((AndroidScreens)_view.TryGetFeature<IScreenImpl>()!).OnChanged();
}
}

22
src/Android/Avalonia.Android/ChoreographerTimer.cs

@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Reactive;
using Avalonia.Rendering;
using static Avalonia.Android.Platform.SkiaPlatform.AndroidFramebuffer;
@ -17,7 +15,6 @@ namespace Avalonia.Android
private readonly TaskCompletionSource<IntPtr> _choreographer = new();
private readonly AutoResetEvent _event = new(false);
private readonly GCHandle _timerHandle;
private readonly HashSet<AvaloniaView> _views = new();
private Action<TimeSpan>? _tick;
private bool _pendingCallback;
private long _lastTime;
@ -49,23 +46,6 @@ namespace Avalonia.Android
}
}
internal IDisposable SubscribeView(AvaloniaView view)
{
lock (_lock)
{
_views.Add(view);
PostFrameCallbackIfNeeded();
}
return Disposable.Create(
() =>
{
lock (_lock)
_views.Remove(view);
}
);
}
private void Loop()
{
Looper.Prepare();
@ -94,7 +74,7 @@ namespace Avalonia.Android
if(_pendingCallback)
return;
if (_tick == null || _views.Count == 0)
if (_tick == null)
return;
_pendingCallback = true;

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

@ -3,35 +3,48 @@ using Android.Content;
using Android.Content.Res;
using Android.Provider;
using Android.Views;
using AndroidX.Core.Content;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.Threading;
using Color = Avalonia.Media.Color;
namespace Avalonia.Android.Platform;
// TODO: ideally should be created per view/activity.
internal class AndroidPlatformSettings : DefaultPlatformSettings
{
private PlatformColorValues _latestValues;
private PlatformColorValues _colorValues;
private TimeSpan _holdWaitDuration = TimeSpan.FromMilliseconds(300);
private TimeSpan _doubleTapTime = TimeSpan.FromMilliseconds(500);
private Size _doubleTapSize = new Size(16,16);
private Size _tapSize = new Size(10,10);
private string? _latestLanguage;
public AndroidPlatformSettings()
{
_latestValues = base.GetColorValues();
if (global::Android.App.Application.Context is { } context)
{
GetInputConfigValues(context);
UpdateInputConfigValues(context);
_colorValues = GetColorValuesFromContext(context);
_latestLanguage = QueryPreferredApplicationLanguage(context);
ContextCompat.RegisterReceiver(
context,
new ConfigurationChangedReceiver(this),
new IntentFilter(Intent.ActionConfigurationChanged),
ContextCompat.ReceiverNotExported);
}
else
_colorValues = base.GetColorValues();
}
public override PlatformColorValues GetColorValues()
{
return _latestValues;
return _colorValues;
}
public override string PreferredApplicationLanguage => _latestLanguage ?? base.PreferredApplicationLanguage;
public override TimeSpan GetDoubleTapTime(PointerType type)
{
return type == PointerType.Mouse ? base.GetDoubleTapTime(type) : _doubleTapTime;
@ -49,50 +62,69 @@ internal class AndroidPlatformSettings : DefaultPlatformSettings
public override TimeSpan HoldWaitDuration => _holdWaitDuration;
internal void OnViewConfigurationChanged(Context context, Configuration configuration)
private void UpdateColorValues(Context context)
{
if (context.Resources is null)
var oldColorValues = _colorValues;
var colorValues = GetColorValuesFromContext(context);
if (oldColorValues != colorValues)
{
return;
_colorValues = colorValues;
OnColorValuesChanged(colorValues);
}
}
PlatformThemeVariant systemTheme = (configuration.UiMode & UiMode.NightMask) switch
private void UpdatePreferredApplicationLanguage(Context context)
{
var oldLanguage = _latestLanguage;
var language = QueryPreferredApplicationLanguage(context);
if (oldLanguage != language)
{
UiMode.NightYes => PlatformThemeVariant.Dark,
UiMode.NightNo => PlatformThemeVariant.Light,
_ => throw new ArgumentOutOfRangeException()
};
_latestLanguage = language;
OnPreferredApplicationLanguageChanged();
}
}
private static PlatformColorValues GetColorValuesFromContext(Context context)
{
var uiMode = context.Resources?.Configuration?.UiMode & UiMode.NightMask ?? UiMode.NightNo;
var systemTheme = uiMode == UiMode.NightYes ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light;
var contrastPreference = IsHighContrast(context);
if (OperatingSystem.IsAndroidVersionAtLeast(31))
{
// See https://developer.android.com/reference/android/R.color
var accent1 = context.Resources.GetColor(17170494, context.Theme); // Resource.Color.SystemAccent1500
var accent2 = context.Resources.GetColor(17170507, context.Theme); // Resource.Color.SystemAccent2500
var accent3 = context.Resources.GetColor(17170520, context.Theme); // Resource.Color.SystemAccent3500
_latestValues = new PlatformColorValues
if (context.Resources is { } resources)
{
ThemeVariant = systemTheme,
ContrastPreference = IsHighContrast(context),
AccentColor1 = new Color(accent1.A, accent1.R, accent1.G, accent1.B),
AccentColor2 = new Color(accent2.A, accent2.R, accent2.G, accent2.B),
AccentColor3 = new Color(accent3.A, accent3.R, accent3.G, accent3.B),
};
// See https://developer.android.com/reference/android/R.color
var accent1 = resources.GetColor(17170494, context.Theme); // Resource.Color.SystemAccent1500
var accent2 = resources.GetColor(17170507, context.Theme); // Resource.Color.SystemAccent2500
var accent3 = resources.GetColor(17170520, context.Theme); // Resource.Color.SystemAccent3500
return new PlatformColorValues
{
ThemeVariant = systemTheme,
ContrastPreference = contrastPreference,
AccentColor1 = new Color(accent1.A, accent1.R, accent1.G, accent1.B),
AccentColor2 = new Color(accent2.A, accent2.R, accent2.G, accent2.B),
AccentColor3 = new Color(accent3.A, accent3.R, accent3.G, accent3.B),
};
}
}
else if (OperatingSystem.IsAndroidVersionAtLeast(23))
{
// See https://developer.android.com/reference/android/R.attr
var array = context.Theme?.ObtainStyledAttributes(new[] { 16843829 }); // Resource.Attribute.ColorAccent
var array = context.Theme?.ObtainStyledAttributes([16843829]); // Resource.Attribute.ColorAccent
if (array is not null)
{
try
{
var accent = array.GetColor(0, 0);
_latestValues = new PlatformColorValues
return new PlatformColorValues
{
ThemeVariant = systemTheme,
ContrastPreference = IsHighContrast(context),
ContrastPreference = contrastPreference,
AccentColor1 = new Color(accent.A, accent.R, accent.G, accent.B)
};
}
@ -102,17 +134,15 @@ internal class AndroidPlatformSettings : DefaultPlatformSettings
}
}
}
else
{
_latestValues = _latestValues with { ThemeVariant = systemTheme };
}
GetInputConfigValues(context);
OnColorValuesChanged(_latestValues);
return new PlatformColorValues
{
ThemeVariant = systemTheme,
ContrastPreference = contrastPreference
};
}
private void GetInputConfigValues(Context context)
private void UpdateInputConfigValues(Context context)
{
_holdWaitDuration = TimeSpan.FromMilliseconds(ViewConfiguration.LongPressTimeout);
@ -131,6 +161,12 @@ internal class AndroidPlatformSettings : DefaultPlatformSettings
}
}
private string? QueryPreferredApplicationLanguage(Context? context)
{
var locale = context?.Resources?.Configuration?.Locales?.Get(0);
return locale?.ToLanguageTag() is { Length: > 0 } tag ? tag : null;
}
private static ColorContrastPreference IsHighContrast(Context context)
{
try
@ -143,4 +179,27 @@ internal class AndroidPlatformSettings : DefaultPlatformSettings
return ColorContrastPreference.NoPreference;
}
}
private sealed class ConfigurationChangedReceiver(AndroidPlatformSettings settings) : BroadcastReceiver
{
public override void OnReceive(Context? context, Intent? intent)
{
if (context is null)
return;
// The context might still have the old values at this point because they haven't been processed yet.
// Postpone the update 100ms arbitrarily. Not an ideal solution, but sufficient.
var timer = new DispatcherTimer(TimeSpan.FromMilliseconds(100), DispatcherPriority.Normal, Dispatcher.UIThread);
timer.Tick += (_, _) =>
{
timer.Stop();
settings.UpdateInputConfigValues(context);
settings.UpdateColorValues(context);
settings.UpdatePreferredApplicationLanguage(context);
};
timer.Start();
}
}
}

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

@ -30,6 +30,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
class TopLevelImpl : ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy
{
private readonly Context _context;
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
private readonly AndroidMotionEventsHelper _pointerHelper;
private readonly AndroidInputMethod<AvaloniaView> _textInputMethod;
@ -51,6 +52,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
throw new ArgumentException("AvaloniaView.Context must not be null");
}
_context = context;
_view = new SurfaceViewImpl(context, this, placeOnTop);
_textInputMethod = new AndroidInputMethod<AvaloniaView>(avaloniaView);
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
@ -245,22 +247,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform
};
}
// Sets the default app NightMode to AppCompatDelegate.ModeNightFollowSystem when themeVariant is null. This
// allows app to follow the current OS night mode. Using either ModeNightNo or ModeNightYes will force the app
// to use one night mode, ignoring the system's configuration and preventing us from detecting system theme changes
// in View.OnConfigurationChanged. In this case, only Activity.OnConfigurationChanged is called when system theme is
// changed, but we don't have access to that method if the toplevel view is embedded in a custom activity
var nightMode = themeVariant == null ? AppCompatDelegate.ModeNightFollowSystem :
themeVariant == PlatformThemeVariant.Light ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes;
AppCompatDelegate.DefaultNightMode = nightMode;
if (nightMode == AppCompatDelegate.ModeNightFollowSystem && _view?.Context is { } context
&& context.Resources?.Configuration is { } config)
if (_context is AppCompatActivity activity)
{
var settings =
AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>() as AndroidPlatformSettings;
settings?.OnViewConfigurationChanged(context, config);
var nightMode = themeVariant == PlatformThemeVariant.Dark ?
AppCompatDelegate.ModeNightYes :
AppCompatDelegate.ModeNightNo;
// Don't use AppCompatDelegate.DefaultNightMode: doing so will force the app to use one night mode,
// ignoring the system's configuration and preventing us from detecting system theme changes.
activity.Delegate.SetLocalNightMode(nightMode);
}
}

67
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs

@ -7,6 +7,7 @@ using Android;
using Android.App;
using Android.Content;
using Android.Provider;
using AndroidX.DocumentFile.Provider;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using AndroidUri = Android.Net.Uri;
@ -18,6 +19,8 @@ namespace Avalonia.Android.Platform.Storage;
internal class AndroidStorageProvider : IStorageProvider
{
public static ReadOnlySpan<byte> AndroidKey => "android"u8;
private const string ContentUriScheme = "content";
private const string FileUriScheme = "file";
private readonly Activity _activity;
public AndroidStorageProvider(Activity activity)
@ -62,10 +65,29 @@ internal class AndroidStorageProvider : IStorageProvider
// to read or write files in your application-specific directories [...]"
// Consequently, we don't try to check for that permission here anymore.
var javaFile = new JavaFile(androidUriPath);
if (javaFile.Exists() && javaFile.IsFile)
if (filePath.Scheme == FileUriScheme && androidUri.Path is not null)
{
return new BclStorageFile(new System.IO.FileInfo(javaFile.AbsolutePath));
var javaFile = new JavaFile(androidUri.Path);
if (javaFile.Exists() && javaFile.IsFile)
{
return new BclStorageFile(new System.IO.FileInfo(javaFile.AbsolutePath));
}
return null;
}
else if (filePath.Scheme == ContentUriScheme && _activity.ContentResolver is not null)
{
try
{
using var stream = _activity.ContentResolver.OpenInputStream(androidUri);
if (stream is not null)
{
return new AndroidStorageFile(_activity, androidUri);
}
}
catch (Exception)
{
return null;
}
}
return null;
@ -89,10 +111,43 @@ internal class AndroidStorageProvider : IStorageProvider
return null;
}
var javaFile = new JavaFile(androidUriPath);
if (javaFile.Exists() && javaFile.IsDirectory)
if (folderPath.Scheme == FileUriScheme && androidUri.Path is not null)
{
var javaFile = new JavaFile(androidUri.Path);
if (javaFile.Exists() && javaFile.IsDirectory)
{
return new BclStorageFolder(new System.IO.DirectoryInfo(javaFile.AbsolutePath));
}
return null;
}
else if (folderPath.Scheme == ContentUriScheme)
{
return new BclStorageFolder(new System.IO.DirectoryInfo(javaFile.AbsolutePath));
try
{
if (OperatingSystem.IsAndroidVersionAtLeast(21))
{
if (DocumentsContract.IsTreeUri(androidUri))
{
var docId = DocumentsContract.GetTreeDocumentId(androidUri);
if (!string.IsNullOrEmpty(docId))
{
return new AndroidStorageFolder(_activity, androidUri, false);
}
}
else
{
var documentFile = DocumentFile.FromSingleUri(_activity, androidUri);
if (documentFile is not null && documentFile.Exists() && documentFile.IsDirectory)
{
return new AndroidStorageFolder(_activity, androidUri, false);
}
}
}
}
catch (Exception)
{
return null;
}
}
return null;

9
src/Avalonia.Base/Animation/AnimationInstance`1.cs

@ -15,6 +15,7 @@ namespace Avalonia.Animation
private readonly Animator<T> _animator;
private readonly Animation _animation;
private readonly Animatable _targetControl;
private readonly Visual? _visualTarget;
private readonly Action? _onCompleteAction;
private IDisposable? _timerSub;
private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChangedDelegate;
@ -61,7 +62,8 @@ namespace Avalonia.Animation
private readonly bool _shouldPauseOnInvisible;
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action? OnComplete, Func<double, T, T> Interpolator, bool shouldPauseOnInvisible)
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock,
Action? OnComplete, Func<double, T, T> Interpolator, bool shouldPauseOnInvisible, Visual? visualTarget)
{
_lastInterpValue = default!;
_animator = animator;
@ -70,6 +72,7 @@ namespace Avalonia.Animation
_onCompleteAction = OnComplete;
_interpolator = Interpolator;
_baseClock = baseClock;
_visualTarget = visualTarget;
_initialKFValue = default!;
_neutralValue = default!;
_shouldPauseOnInvisible = shouldPauseOnInvisible;
@ -132,7 +135,7 @@ namespace Avalonia.Animation
_targetControl.PropertyChanged -= _propertyChangedDelegate;
timerSub.Dispose();
if (_targetControl is Visual visual)
if (_visualTarget is { } visual)
{
if (_visibilityChangedHandler is not null)
{
@ -155,7 +158,7 @@ namespace Avalonia.Animation
_clock = new Clock(_baseClock);
_timerSub = _clock.Subscribe(Step);
if (_targetControl is Visual visual)
if (_visualTarget is { } visual)
{
if (_shouldPauseOnInvisible)
{

19
src/Avalonia.Base/Animation/Animators/Animator`1.cs

@ -18,8 +18,19 @@ namespace Avalonia.Animation.Animators
/// <inheritdoc/>
public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete, bool shouldPauseOnInvisible)
=> Apply(animation, control, clock, match, onComplete, shouldPauseOnInvisible, control as Visual);
internal IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable<bool> match,
Action? onComplete, bool shouldPauseOnInvisible, Visual? visualTarget)
{
var subject = new DisposeAnimationInstanceSubject<T>(this, animation, control, clock, onComplete, shouldPauseOnInvisible);
var subject = new DisposeAnimationInstanceSubject<T>(
this,
animation,
control,
clock,
onComplete,
shouldPauseOnInvisible,
visualTarget);
return new CompositeDisposable(match.Subscribe(subject), subject);
}
@ -103,7 +114,8 @@ namespace Avalonia.Animation.Animators
/// <summary>
/// Runs the KeyFrames Animation.
/// </summary>
internal IDisposable Run(Animation animation, Animatable control, IClock? clock, Action? onComplete, bool shouldPauseOnInvisible)
internal IDisposable Run(Animation animation, Animatable control, IClock? clock, Action? onComplete,
bool shouldPauseOnInvisible, Visual? visualTarget)
{
var instance = new AnimationInstance<T>(
animation,
@ -112,7 +124,8 @@ namespace Avalonia.Animation.Animators
clock ?? control.Clock ?? Clock.GlobalClock,
onComplete,
InterpolationHandler,
shouldPauseOnInvisible);
shouldPauseOnInvisible,
visualTarget);
return BindAnimation(control, instance);
}

4
src/Avalonia.Base/Animation/Animators/TransformAnimator.cs

@ -67,7 +67,7 @@ namespace Avalonia.Animation.Animators
if (renderTransformType == Property.OwnerType)
{
return _doubleAnimator.Apply(animation, (Transform) ctrl.RenderTransform, clock ?? control.Clock,
obsMatch, onComplete, shouldPauseOnInvisible);
obsMatch, onComplete, shouldPauseOnInvisible, ctrl);
}
// It's a TransformGroup and try finding the target there.
else if (renderTransformType == typeof(TransformGroup))
@ -77,7 +77,7 @@ namespace Avalonia.Animation.Animators
if (transform.GetType() == Property.OwnerType)
{
return _doubleAnimator.Apply(animation, transform, clock ?? control.Clock,
obsMatch, onComplete, shouldPauseOnInvisible);
obsMatch, onComplete, shouldPauseOnInvisible, ctrl);
}
}
}

6
src/Avalonia.Base/Animation/CrossFade.cs

@ -260,9 +260,7 @@ namespace Avalonia.Animation
return FarPeekOpacity;
}
private static double Lerp(double from, double to, double t)
{
return from + ((to - from) * Math.Clamp(t, 0.0, 1.0));
}
private static double Lerp(double from, double to, double t) =>
double.Lerp(from, to, Math.Clamp(t, 0.0, 1.0));
}
}

11
src/Avalonia.Base/Animation/DisposeAnimationInstanceSubject.cs

@ -12,7 +12,8 @@ namespace Avalonia.Animation
Animatable control,
IClock? clock,
Action? onComplete,
bool shouldPauseOnInvisible)
bool shouldPauseOnInvisible,
Visual? visualTarget)
: IObserver<bool>, IDisposable
{
private IDisposable? _lastInstance;
@ -41,7 +42,13 @@ namespace Avalonia.Animation
if (matchVal)
{
_lastInstance = animator.Run(animation, control, clock, onComplete, shouldPauseOnInvisible);
_lastInstance = animator.Run(
animation,
control,
clock,
onComplete,
shouldPauseOnInvisible,
visualTarget);
}
else
{

6
src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs

@ -244,10 +244,8 @@ public class Rotate3DTransition : PageSlide
return 1;
}
private static double Lerp(double from, double to, double t)
{
return from + ((to - from) * Math.Clamp(t, 0.0, 1.0));
}
private static double Lerp(double from, double to, double t) =>
double.Lerp(from, to, Math.Clamp(t, 0.0, 1.0));
/// <inheritdoc/>
public override void Reset(Visual visual)

50
src/Avalonia.Base/Data/Converters/ObjectConverters.cs

@ -1,3 +1,6 @@
using System.Linq;
using System.Collections.Generic;
namespace Avalonia.Data.Converters
{
/// <summary>
@ -28,5 +31,52 @@ namespace Avalonia.Data.Converters
/// </summary>
public static readonly IValueConverter NotEqual =
new FuncValueConverter<object?, object?, bool>((a, b) => !a?.Equals(b) ?? b is not null);
/// <summary>
/// A multi-value converter that returns true if all inputs are null.
/// </summary>
public static readonly IMultiValueConverter AreAllNull =
new FuncMultiValueConverter<object?, bool>(x => x.All(item => item is null));
/// <summary>
/// A multi-value converter that returns true if at least one input is null.
/// </summary>
public static readonly IMultiValueConverter AreAnyNull =
new FuncMultiValueConverter<object?, bool>(x => x.Any(item => item is null));
/// <summary>
/// A multi-value converter that returns true if all inputs are equal to each other.
/// Null values are not considered equal to anything since they don't have Equals method.
/// </summary>
public static readonly IMultiValueConverter AreAllEqual =
new FuncMultiValueConverter<object?, bool>(EqualityFunction);
/// <summary>
/// Helper function for AreAllEqual converter. Returns true if all inputs are equal to each other.
/// </summary>
/// <param name="values">The values to compare.</param>
/// <returns>True if all values are equal, false otherwise.</returns>
private static bool EqualityFunction(IEnumerable<object?> values)
{
using var enumerator = values.GetEnumerator();
//Empty collection is considered equal.
if (!enumerator.MoveNext())
return true;
//Null values are not considered equal to anything since they don't have Equals method.
var first = enumerator.Current;
if (first is null)
return false;
while (enumerator.MoveNext())
{
var item = enumerator.Current;
if (item is null || !item.Equals(first))
return false;
}
return true;
}
}
}

60
src/Avalonia.Base/GeneratedAttachedPropertyAttribute.cs

@ -0,0 +1,60 @@
using System;
using Avalonia.Data;
namespace Avalonia;
/// <summary>
/// Instructs the Avalonia property source generator to emit an <see cref="AttachedProperty{TValue}"/>
/// registration and accessor methods for the annotated partial <c>Get</c> method.
/// </summary>
/// <remarks>
/// <para>
/// Apply to a <c>static partial T Get{Name}(THost element)</c> method declared on a <c>partial</c> class.
/// </para>
/// </remarks>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class GeneratedAttachedPropertyAttribute : Attribute
{
/// <summary>
/// Re-owns an existing attached property from the specified base type instead of registering a new property.
/// The source property must be a static <c>{Name}Property</c> member of the given type.
/// Metadata may still be overridden with <see cref="DefaultValue"/>, <see cref="DefaultBindingMode"/> and <see cref="CoerceMethodName"/>.
/// </summary>
public Type? AddOwnerFrom { get; set; }
/// <summary>
/// The default value of the property. Must be a compile-time constant.
/// For non-constant defaults, call <c>{Name}Property.OverrideDefaultValue(...)</c> in a static constructor instead.
/// </summary>
public object? DefaultValue { get; set; }
/// <summary>
/// The default binding mode for the property.
/// </summary>
public BindingMode DefaultBindingMode { get; set; }
/// <summary>
/// Whether the property inherits its value down the logical tree.
/// </summary>
/// <remarks>
/// Cannot be used with <see cref="AddOwnerFrom"/>.
/// </remarks>
public bool Inherits { get; set; }
/// <summary>
/// The name of the validation method, with signature
/// <c>static bool {Method}(T value)</c>. Pass via <c>nameof(...)</c>.
/// Declare it as an <c>private static</c> method on the same type.
/// </summary>
/// <remarks>
/// Cannot be used with <see cref="AddOwnerFrom"/>.
/// </remarks>
public string? ValidateMethodName { get; set; }
/// <summary>
/// The name of the coercion method, with signature
/// <c>static T {Method}(AvaloniaObject sender, T value)</c>.
/// Declare it as an <c>private static</c> method on the same type.
/// </summary>
public string? CoerceMethodName { get; set; }
}

41
src/Avalonia.Base/GeneratedDirectPropertyAttribute.cs

@ -0,0 +1,41 @@
using System;
using Avalonia.Data;
namespace Avalonia;
/// <summary>
/// Instructs the Avalonia property source generator to emit a <see cref="DirectProperty{TOwner, TValue}"/>
/// registration and accessor bodies for the annotated partial property.
/// </summary>
/// <remarks>
/// <para>
/// Apply to a <c>partial</c> instance property with both <c>get</c> and <c>set</c> accessors,
/// declared on a <c>partial</c> class deriving from <see cref="AvaloniaObject"/>.
/// </para>
/// </remarks>
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public sealed class GeneratedDirectPropertyAttribute : Attribute
{
/// <summary>
/// Re-owns an existing direct property from the specified base type instead of registering a new property.
/// The source property must be a static <c>{Name}Property</c> member of the given type.
/// </summary>
public Type? AddOwnerFrom { get; set; }
/// <summary>
/// The value returned when the property is cleared (e.g. by <c>ClearValue</c>)
/// or when a binding produces an invalid value.
/// The initial value of the property comes from the property initializer.
/// </summary>
public object? UnsetValue { get; set; }
/// <summary>
/// The default binding mode for the property.
/// </summary>
public BindingMode DefaultBindingMode { get; set; }
/// <summary>
/// Whether the property is interested in data validation.
/// </summary>
public bool EnableDataValidation { get; set; }
}

66
src/Avalonia.Base/GeneratedStyledPropertyAttribute.cs

@ -0,0 +1,66 @@
using System;
using Avalonia.Data;
namespace Avalonia;
/// <summary>
/// Instructs the Avalonia property source generator to emit a <see cref="StyledProperty{TValue}"/>
/// registration and accessor bodies for the annotated partial property.
/// </summary>
/// <remarks>
/// <para>
/// Apply to a <c>partial</c> instance property with both <c>get</c> and <c>set</c> accessors,
/// declared on a <c>partial</c> class deriving from <see cref="AvaloniaObject"/>.
/// </para>
/// </remarks>
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public sealed class GeneratedStyledPropertyAttribute : Attribute
{
/// <summary>
/// Re-owns an existing styled property from the specified base type instead of registering a new property.
/// The source property must be a static <c>{Name}Property</c> member of the given type.
/// Metadata may still be overridden with <see cref="DefaultValue"/>, <see cref="DefaultBindingMode"/> and <see cref="CoerceMethodName"/>.
/// </summary>
public Type? AddOwnerFrom { get; set; }
/// <summary>
/// The default value of the property. Must be a compile-time constant.
/// For non-constant defaults, call <c>{Name}Property.OverrideDefaultValue(...)</c> in a static constructor instead.
/// </summary>
public object? DefaultValue { get; set; }
/// <summary>
/// The default binding mode for the property.
/// </summary>
public BindingMode DefaultBindingMode { get; set; }
/// <summary>
/// Whether the property inherits its value down the logical tree.
/// </summary>
/// <remarks>
/// Cannot be used with <see cref="AddOwnerFrom"/>.
/// </remarks>
public bool Inherits { get; set; }
/// <summary>
/// Whether the property is interested in data validation.
/// </summary>
public bool EnableDataValidation { get; set; }
/// <summary>
/// The name of the validation method, with signature
/// <c>static bool {Method}(T value)</c>.
/// Declare it as an <c>private static</c> method on the same type.
/// </summary>
/// <remarks>
/// Cannot be used with <see cref="AddOwnerFrom"/>.
/// </remarks>
public string? ValidateMethodName { get; set; }
/// <summary>
/// The name of the coercion method, with signature
/// <c>static T {Method}(AvaloniaObject sender, T value)</c>.
/// Declare it as an <c>private static</c> method on the same type.
/// </summary>
public string? CoerceMethodName { get; set; }
}

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

@ -18,6 +18,8 @@ namespace Avalonia.Input
private static MouseDevice? _primary;
internal static MouseDevice Primary => _primary ??= new MouseDevice();
internal static void ResetPrimaryForUnitTests() => _primary = null;
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
@ -218,9 +220,7 @@ namespace Avalonia.Input
}
finally
{
_pointer.Capture(null, CaptureSource.Implicit);
_pointer.CaptureGestureRecognizer(null);
_pointer.IsGestureRecognitionSkipped = false;
_pointer.CaptureLost(CaptureSource.Implicit);
_lastMouseDownButton = default;
}
return e.Handled;

9
src/Avalonia.Base/Input/PenDevice.cs

@ -63,6 +63,9 @@ namespace Avalonia.Input
case RawPointerEventType.LeaveWindow:
shouldReleasePointer = true;
break;
case RawPointerEventType.CancelCapture:
pointer.PlatformCaptureLost();
break;
case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown:
case RawPointerEventType.MiddleButtonDown:
@ -90,8 +93,8 @@ namespace Avalonia.Input
{
if (shouldReleasePointer)
{
pointer.Dispose();
_pointers.Remove(e.RawPointerId);
pointer.Dispose();
}
}
}
@ -173,9 +176,7 @@ namespace Avalonia.Input
}
finally
{
pointer.Capture(null, CaptureSource.Implicit);
pointer.CaptureGestureRecognizer(null);
pointer.IsGestureRecognitionSkipped = false;
pointer.CaptureLost(CaptureSource.Implicit);
_lastMouseDownButton = default;
}

53
src/Avalonia.Base/Input/Pointer.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Avalonia.Input.GestureRecognizers;
using Avalonia.VisualTree;
@ -18,6 +19,8 @@ namespace Avalonia.Input
private static int s_NextFreePointerId = 1000;
public static int GetNextFreeId() => s_NextFreePointerId++;
private bool _disposed;
public Pointer(int id, PointerType type, bool isPrimary)
{
Id = id;
@ -40,10 +43,25 @@ namespace Avalonia.Input
}
internal void PlatformCaptureLost()
internal void PlatformCaptureLost() => CaptureLost(CaptureSource.Platform);
/// <summary>
/// Ends every capture the pointer holds, on the element and on a gesture recognizer.
/// </summary>
internal void CaptureLost(CaptureSource source)
{
if (_disposed)
return;
CaptureLostCore(source);
}
private void CaptureLostCore(CaptureSource source)
{
if (Captured != null)
Capture(null, CaptureSource.Platform);
CaptureCore(null, source);
CaptureGestureRecognizerCore(null);
IsGestureRecognitionSkipped = false;
}
public void Capture(IInputElement? control)
@ -52,6 +70,17 @@ namespace Avalonia.Input
}
internal void Capture(IInputElement? control, CaptureSource source)
{
if (_disposed)
{
Debug.Assert(control is null, "Capturing a pointer that no longer exists.");
return;
}
CaptureCore(control, source);
}
private void CaptureCore(IInputElement? control, CaptureSource source)
{
var oldCapture = Captured;
var oldSource = CaptureSource;
@ -148,7 +177,14 @@ namespace Avalonia.Input
public void Dispose()
{
// callers are responsible for calling Capture(null, source) with an appropriate source
if (_disposed)
return;
// Mark the pointer gone first, so a capture lost handler can't capture it again.
// It no longer exists, so the platform is the only source the release can come from.
_disposed = true;
CaptureLostCore(CaptureSource.Platform);
}
/// <summary>
@ -156,6 +192,17 @@ namespace Avalonia.Input
/// </summary>
/// <param name="gestureRecognizer">The gesture recognizer.</param>
internal void CaptureGestureRecognizer(GestureRecognizer? gestureRecognizer)
{
if (_disposed)
{
Debug.Assert(gestureRecognizer is null, "Capturing a pointer that no longer exists to a gesture recognizer.");
return;
}
CaptureGestureRecognizerCore(gestureRecognizer);
}
private void CaptureGestureRecognizerCore(GestureRecognizer? gestureRecognizer)
{
if (CapturedGestureRecognizer != gestureRecognizer)
{

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

@ -109,20 +109,14 @@ namespace Avalonia.Input
{
target.RaiseEvent(e);
}
pointer?.Capture(null, CaptureSource.Implicit);
pointer?.CaptureLost(CaptureSource.Implicit);
}
}
if (args.Type == RawPointerEventType.TouchCancel)
{
_pointers.Remove(args.RawPointerId);
using (pointer)
{
pointer?.Capture(null, CaptureSource.Platform);
pointer?.CaptureGestureRecognizer(null);
if (pointer != null)
pointer.IsGestureRecognitionSkipped = false;
}
pointer?.Dispose();
}
if (args.Type == RawPointerEventType.TouchUpdate)
@ -165,7 +159,7 @@ namespace Avalonia.Input
internal void PlatformCaptureLost()
{
foreach (var pointer in _pointers.Values)
pointer.Capture(null);
pointer.PlatformCaptureLost();
}
}
}

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

Loading…
Cancel
Save