Browse Source

Merge branch 'master' into fixes/macCaretPosition

pull/11021/head
Benedikt Stebner 3 years ago
committed by GitHub
parent
commit
719e7b1fa6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      Avalonia.Desktop.slnf
  2. 42
      Avalonia.sln
  3. 341
      native/Avalonia.Native/src/OSX/platformthreading.mm
  4. 4
      samples/BindingDemo/MainWindow.xaml
  5. 2
      samples/ControlCatalog.Android/MainActivity.cs
  6. 4
      samples/ControlCatalog.Android/Resources/values-night/colors.xml
  7. 3
      samples/ControlCatalog/MainView.xaml
  8. 16
      samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
  9. 35
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  10. 222
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml
  11. 68
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs
  12. 290
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml
  13. 37
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
  14. 2
      samples/ControlCatalog/Pages/TabControlPage.xaml
  15. 40
      samples/RenderDemo/Pages/AnimationsPage.xaml
  16. BIN
      samples/SafeAreaDemo.Android/Icon.png
  17. 11
      samples/SafeAreaDemo.Android/MainActivity.cs
  18. 5
      samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml
  19. 13
      samples/SafeAreaDemo.Android/Resources/drawable/splash_screen.xml
  20. 4
      samples/SafeAreaDemo.Android/Resources/values/colors.xml
  21. 17
      samples/SafeAreaDemo.Android/Resources/values/styles.xml
  22. 24
      samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj
  23. 30
      samples/SafeAreaDemo.Android/SplashActivity.cs
  24. 21
      samples/SafeAreaDemo.Desktop/Program.cs
  25. 24
      samples/SafeAreaDemo.Desktop/SafeAreaDemo.Desktop.csproj
  26. 18
      samples/SafeAreaDemo.Desktop/app.manifest
  27. 17
      samples/SafeAreaDemo.iOS/AppDelegate.cs
  28. 5
      samples/SafeAreaDemo.iOS/Entitlements.plist
  29. 47
      samples/SafeAreaDemo.iOS/Info.plist
  30. 15
      samples/SafeAreaDemo.iOS/Main.cs
  31. 43
      samples/SafeAreaDemo.iOS/Resources/LaunchScreen.xib
  32. 18
      samples/SafeAreaDemo.iOS/SafeAreaDemo.iOS.csproj
  33. 15
      samples/SafeAreaDemo/App.xaml
  34. 36
      samples/SafeAreaDemo/App.xaml.cs
  35. BIN
      samples/SafeAreaDemo/Assets/avalonia-logo.ico
  36. 27
      samples/SafeAreaDemo/SafeAreaDemo.csproj
  37. 31
      samples/SafeAreaDemo/ViewLocator.cs
  38. 112
      samples/SafeAreaDemo/ViewModels/MainViewModel.cs
  39. 52
      samples/SafeAreaDemo/Views/MainView.xaml
  40. 25
      samples/SafeAreaDemo/Views/MainView.xaml.cs
  41. 12
      samples/SafeAreaDemo/Views/MainWindow.xaml
  42. 13
      samples/SafeAreaDemo/Views/MainWindow.xaml.cs
  43. 14
      samples/VirtualizationDemo/App.axaml
  44. 20
      samples/VirtualizationDemo/App.axaml.cs
  45. 7
      samples/VirtualizationDemo/App.xaml
  46. 21
      samples/VirtualizationDemo/App.xaml.cs
  47. 190
      samples/VirtualizationDemo/Assets/chat.json
  48. 20
      samples/VirtualizationDemo/MainWindow.axaml
  49. 15
      samples/VirtualizationDemo/MainWindow.axaml.cs
  50. 64
      samples/VirtualizationDemo/MainWindow.xaml
  51. 22
      samples/VirtualizationDemo/MainWindow.xaml.cs
  52. 23
      samples/VirtualizationDemo/Models/Chat.cs
  53. 19
      samples/VirtualizationDemo/Program.cs
  54. 17
      samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs
  55. 21
      samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs
  56. 17
      samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs
  57. 26
      samples/VirtualizationDemo/ViewModels/ItemViewModel.cs
  58. 164
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  59. 17
      samples/VirtualizationDemo/ViewModels/PlaygroundItemViewModel.cs
  60. 95
      samples/VirtualizationDemo/ViewModels/PlaygroundPageViewModel.cs
  61. 39
      samples/VirtualizationDemo/Views/ChatPageView.axaml
  62. 11
      samples/VirtualizationDemo/Views/ChatPageView.axaml.cs
  63. 18
      samples/VirtualizationDemo/Views/ExpanderPageView.axaml
  64. 13
      samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs
  65. 66
      samples/VirtualizationDemo/Views/PlaygroundPageView.axaml
  66. 44
      samples/VirtualizationDemo/Views/PlaygroundPageView.axaml.cs
  67. 21
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  68. 6
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  69. 1
      src/Android/Avalonia.Android/AndroidPlatform.cs
  70. 2
      src/Android/Avalonia.Android/AndroidThreadingInterface.cs
  71. 4
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  72. 49
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  73. 30
      src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
  74. 22
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  75. 41
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  76. 6
      src/Avalonia.Base/AvaloniaProperty.cs
  77. 14
      src/Avalonia.Base/CombinedGeometry.cs
  78. 26
      src/Avalonia.Base/Controls/ResourceDictionary.cs
  79. 2
      src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs
  80. 5
      src/Avalonia.Base/DirectPropertyBase.cs
  81. 53
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  82. 24
      src/Avalonia.Base/Layout/LayoutManager.cs
  83. 2
      src/Avalonia.Base/Layout/Layoutable.cs
  84. 2
      src/Avalonia.Base/Media/Color.cs
  85. 4
      src/Avalonia.Base/Media/DrawingContext.cs
  86. 22
      src/Avalonia.Base/Media/Effects/BlurEffect.cs
  87. 104
      src/Avalonia.Base/Media/Effects/DropShadowEffect.cs
  88. 93
      src/Avalonia.Base/Media/Effects/Effect.cs
  89. 131
      src/Avalonia.Base/Media/Effects/EffectAnimator.cs
  90. 18
      src/Avalonia.Base/Media/Effects/EffectConverter.cs
  91. 56
      src/Avalonia.Base/Media/Effects/EffectExtesions.cs
  92. 83
      src/Avalonia.Base/Media/Effects/EffectTransition.cs
  93. 29
      src/Avalonia.Base/Media/Effects/IBlurEffect.cs
  94. 84
      src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs
  95. 26
      src/Avalonia.Base/Media/Effects/IEffect.cs
  96. 62
      src/Avalonia.Base/Media/FontManager.cs
  97. 221
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  98. 259
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  99. 17
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  100. 68
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

7
Avalonia.Desktop.slnf

@ -38,13 +38,14 @@
"src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj",
"src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj",
"src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj",
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",
"src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj",
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",

42
Avalonia.sln

@ -244,13 +244,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo", "samples\SafeAreaDemo\SafeAreaDemo.csproj", "{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Android", "samples\SafeAreaDemo.Android\SafeAreaDemo.Android.csproj", "{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Desktop", "samples\SafeAreaDemo.Desktop\SafeAreaDemo.Desktop.csproj", "{4CDAD037-34A2-4CCF-A03A-C6C7B988A572}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.iOS", "samples\SafeAreaDemo.iOS\SafeAreaDemo.iOS.csproj", "{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -595,6 +603,26 @@ Global
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Release|Any CPU.Build.0 = Release|Any CPU
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Release|Any CPU.Build.0 = Release|Any CPU
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Release|Any CPU.Deploy.0 = Release|Any CPU
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Release|Any CPU.Build.0 = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Build.0 = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Deploy.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -661,10 +689,14 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

341
native/Avalonia.Native/src/OSX/platformthreading.mm

@ -1,193 +1,266 @@
#include "common.h"
class PlatformThreadingInterface;
class LoopCancellation : public ComSingleObject<IAvnLoopCancellation, &IID_IAvnLoopCancellation>
{
public:
FORWARD_IUNKNOWN()
bool Running = false;
bool Cancelled = false;
bool IsApp = false;
virtual void Cancel() override
{
Cancelled = true;
if(Running)
{
Running = false;
if(![NSThread isMainThread])
{
AddRef();
dispatch_async(dispatch_get_main_queue(), ^{
if(Release() == 0)
return;
Cancel();
});
return;
};
if(IsApp)
[NSApp stop:nil];
else
{
// Wakeup the event loop
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:YES];
}
}
};
};
// CFRunLoopTimerSetNextFireDate docs recommend to "create a repeating timer with an initial
// firing time in the distant future (or the initial firing time) and a very large repeat
// interval—on the order of decades or more"
static double distantFutureInterval = (double)50*365*24*3600;
@interface Signaler : NSObject
-(void) setParent: (PlatformThreadingInterface*)parent;
-(void) signal: (int) priority;
-(void) setEvents:(IAvnPlatformThreadingInterfaceEvents*) events;
-(void) updateTimer:(int)ms;
-(Signaler*) init;
-(void) destroyObserver;
-(void) signal;
@end
@implementation ActionCallback
@implementation Signaler
{
ComPtr<IAvnActionCallback> _callback;
ComPtr<IAvnPlatformThreadingInterfaceEvents> _events;
bool _wakeupDelegateSent;
bool _signaled;
bool _backgroundProcessingRequested;
CFRunLoopObserverRef _observer;
CFRunLoopTimerRef _timer;
}
- (void) checkSignaled
{
bool signaled;
@synchronized (self) {
signaled = _signaled;
_signaled = false;
}
if(signaled)
{
_events->Signaled();
}
}
- (ActionCallback*) initWithCallback: (IAvnActionCallback*) callback
- (Signaler*) init
{
_callback = callback;
_observer = CFRunLoopObserverCreateWithHandler(nil,
kCFRunLoopBeforeSources
| kCFRunLoopAfterWaiting
| kCFRunLoopBeforeWaiting
,
true, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if(activity == kCFRunLoopBeforeWaiting)
{
bool triggerProcessing;
@synchronized (self) {
triggerProcessing = self->_backgroundProcessingRequested;
self->_backgroundProcessingRequested = false;
}
if(triggerProcessing)
self->_events->ReadyForBackgroundProcessing();
}
[self checkSignaled];
});
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
_timer = CFRunLoopTimerCreateWithHandler(nil, CFAbsoluteTimeGetCurrent() + distantFutureInterval, distantFutureInterval, 0, 0, ^(CFRunLoopTimerRef timer) {
self->_events->Timer();
});
CFRunLoopAddTimer(CFRunLoopGetMain(), _timer, kCFRunLoopCommonModes);
return self;
}
- (void) action
- (void) destroyObserver
{
_callback->Run();
if(_observer != nil)
{
CFRunLoopObserverInvalidate(_observer);
CFRelease(_observer);
_observer = nil;
}
if(_timer != nil)
{
CFRunLoopTimerInvalidate(_timer);
CFRelease(_timer);
_timer = nil;
}
}
-(void) updateTimer:(int)ms
{
if(_timer == nil)
return;
double interval = ms < 0 ? distantFutureInterval : ((double)ms / 1000);
CFRunLoopTimerSetTolerance(_timer, 0);
CFRunLoopTimerSetNextFireDate(_timer, CFAbsoluteTimeGetCurrent() + interval);
}
@end
- (void) setEvents: (IAvnPlatformThreadingInterfaceEvents*) events
{
_events = events;
}
class TimerWrapper : public ComUnknownObject
- (void) signal
{
NSTimer* _timer;
public:
TimerWrapper(IAvnActionCallback* callback, int ms)
{
auto cb = [[ActionCallback alloc] initWithCallback:callback];
_timer = [NSTimer scheduledTimerWithTimeInterval:(NSTimeInterval)(double)ms/1000 target:cb selector:@selector(action) userInfo:nullptr repeats:true];
@synchronized (self) {
if(_signaled)
return;
_signaled = true;
dispatch_async(dispatch_get_main_queue(), ^{
[self checkSignaled];
});
CFRunLoopWakeUp(CFRunLoopGetMain());
}
virtual ~TimerWrapper()
{
[_timer invalidate];
}
- (void) requestBackgroundProcessing
{
@synchronized (self) {
if(_backgroundProcessingRequested)
return;
_backgroundProcessingRequested = true;
dispatch_async(dispatch_get_main_queue(), ^{
// This is needed to wakeup the loop if we are called from inside of BeforeWait hook
});
}
};
}
@end
class PlatformThreadingInterface : public ComSingleObject<IAvnPlatformThreadingInterface, &IID_IAvnPlatformThreadingInterface>
{
private:
ComPtr<IAvnPlatformThreadingInterfaceEvents> _events;
Signaler* _signaler;
bool _wasRunningAtLeastOnce = false;
class LoopCancellation : public ComSingleObject<IAvnLoopCancellation, &IID_IAvnLoopCancellation>
{
public:
FORWARD_IUNKNOWN()
bool Running = false;
bool Cancelled = false;
virtual void Cancel() override
{
Cancelled = true;
if(Running)
{
Running = false;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSApplication sharedApplication] stop:nil];
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:YES];
});
}
}
};
CFRunLoopObserverRef _observer = nil;
public:
FORWARD_IUNKNOWN()
ComPtr<IAvnSignaledCallback> SignaledCallback;
PlatformThreadingInterface()
{
_signaler = [Signaler new];
[_signaler setParent:this];
}
};
~PlatformThreadingInterface()
{
if(_signaler)
[_signaler setParent: NULL];
_signaler = NULL;
[_signaler destroyObserver];
}
virtual bool GetCurrentThreadIsLoopThread() override
bool GetCurrentThreadIsLoopThread() override
{
return [NSThread isMainThread];
}
virtual void SetSignaledCallback(IAvnSignaledCallback* cb) override
};
void SetEvents(IAvnPlatformThreadingInterfaceEvents *cb) override
{
SignaledCallback = cb;
}
virtual IAvnLoopCancellation* CreateLoopCancellation() override
_events = cb;
[_signaler setEvents:cb];
};
IAvnLoopCancellation *CreateLoopCancellation() override
{
return new LoopCancellation();
}
};
virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) override
void RunLoop(IAvnLoopCancellation *cancel) override
{
START_COM_CALL;
auto can = dynamic_cast<LoopCancellation*>(cancel);
if(can->Cancelled)
return S_OK;
if(_wasRunningAtLeastOnce)
return E_FAIL;
return;
can->Running = true;
_wasRunningAtLeastOnce = true;
[NSApp run];
return S_OK;
}
if(![NSApp isRunning])
{
can->IsApp = true;
[NSApp run];
return;
}
else
{
while(!can->Cancelled)
{
@autoreleasepool
{
NSEvent* ev = [NSApp
nextEventMatchingMask:NSEventMaskAny
untilDate: [NSDate dateWithTimeIntervalSinceNow:1]
inMode:NSDefaultRunLoopMode
dequeue:true];
if(ev != NULL)
[NSApp sendEvent:ev];
}
}
}
};
virtual void Signal(int priority) override
void Signal() override
{
[_signaler signal:priority];
}
[_signaler signal];
};
virtual IUnknown* StartTimer(int priority, int ms, IAvnActionCallback* callback) override
{
@autoreleasepool {
return new TimerWrapper(callback, ms);
}
}
};
@implementation Signaler
PlatformThreadingInterface* _parent = 0;
bool _signaled = 0;
NSArray<NSString*>* _modes;
-(Signaler*) init
{
if(self = [super init])
void UpdateTimer(int ms) override
{
_modes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil];
}
return self;
}
-(void) perform
{
ComPtr<IAvnSignaledCallback> cb;
@synchronized (self) {
_signaled = false;
if(_parent != NULL)
cb = _parent->SignaledCallback;
}
if(cb != nullptr)
cb->Signaled(0, false);
}
-(void) setParent:(PlatformThreadingInterface *)parent
{
@synchronized (self) {
_parent = parent;
}
}
-(void) signal: (int) priority
{
@synchronized (self) {
if(_signaled)
return;
_signaled = true;
[self performSelector:@selector(perform) onThread:[NSThread mainThread] withObject:NULL waitUntilDone:false modes:_modes];
[_signaler updateTimer:ms];
};
void RequestBackgroundProcessing() override {
[_signaler requestBackgroundProcessing];
}
}
@end
};
extern IAvnPlatformThreadingInterface* CreatePlatformThreading()
{

4
samples/BindingDemo/MainWindow.xaml

@ -75,11 +75,11 @@
</StackPanel.DataTemplates>
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
<ListBox ItemsSource="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
</StackPanel>
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
<ListBox ItemsSource="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
</StackPanel>
<ContentControl Content="{ReflectionBinding Selection.SelectedItems[0]}">
<ContentControl.DataTemplates>

2
samples/ControlCatalog.Android/MainActivity.cs

@ -5,7 +5,7 @@ using Avalonia.Android;
namespace ControlCatalog.Android
{
[Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
[Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
public class MainActivity : AvaloniaMainActivity
{
}

4
samples/ControlCatalog.Android/Resources/values-night/colors.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background">#212121</color>
</resources>

3
samples/ControlCatalog/MainView.xaml

@ -147,9 +147,6 @@
<TabItem Header="ScrollViewer">
<pages:ScrollViewerPage />
</TabItem>
<TabItem Header="ScrollViewer Snapping">
<pages:ScrollSnapPage />
</TabItem>
<TabItem Header="Slider">
<pages:SliderPage />
</TabItem>

16
samples/ControlCatalog/Pages/ClipboardPage.xaml.cs

@ -32,13 +32,13 @@ namespace ControlCatalog.Pages
private async void CopyText(object? sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard && ClipboardContent is { } clipboardContent)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard && ClipboardContent is { } clipboardContent)
await clipboard.SetTextAsync(clipboardContent.Text ?? String.Empty);
}
private async void PasteText(object? sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
ClipboardContent.Text = await clipboard.GetTextAsync();
}
@ -46,7 +46,7 @@ namespace ControlCatalog.Pages
private async void CopyTextDataObject(object? sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var dataObject = new DataObject();
dataObject.Set(DataFormats.Text, ClipboardContent.Text ?? string.Empty);
@ -56,7 +56,7 @@ namespace ControlCatalog.Pages
private async void PasteTextDataObject(object? sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
ClipboardContent.Text = await clipboard.GetDataAsync(DataFormats.Text) as string ?? string.Empty;
}
@ -64,7 +64,7 @@ namespace ControlCatalog.Pages
private async void CopyFilesDataObject(object? sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var storageProvider = TopLevel.GetTopLevel(this)!.StorageProvider;
var filesPath = (ClipboardContent.Text ?? string.Empty)
@ -110,7 +110,7 @@ namespace ControlCatalog.Pages
private async void PasteFilesDataObject(object? sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var files = await clipboard.GetDataAsync(DataFormats.Files) as IEnumerable<Avalonia.Platform.Storage.IStorageItem>;
@ -120,7 +120,7 @@ namespace ControlCatalog.Pages
private async void GetFormats(object sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var formats = await clipboard.GetFormatsAsync();
ClipboardContent.Text = string.Join(Environment.NewLine, formats);
@ -129,7 +129,7 @@ namespace ControlCatalog.Pages
private async void Clear(object sender, RoutedEventArgs args)
{
if (Application.Current!.Clipboard is { } clipboard)
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
await clipboard.ClearAsync();
}

35
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -28,6 +28,41 @@
<Grid Grid.Column="2"
Grid.Row="0"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid.Resources>
<x:Double x:Key="ColorSliderSize">24</x:Double>
<x:Double x:Key="ColorSliderTrackSize">18</x:Double>
<CornerRadius x:Key="ColorSliderCornerRadius">12</CornerRadius>
<CornerRadius x:Key="ColorSliderTrackCornerRadius">9</CornerRadius>
<!-- Due to 'SystemControlForegroundBaseHighBrush' usage this only works in Fluent theme. -->
<!-- Otherwise it would be necessary to make custom light/dark resources. -->
<ControlTheme x:Key="ColorSliderThumbTheme"
TargetType="Thumb">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="BorderThickness" Value="5" />
<Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Ellipse Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Fill="Transparent"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="1" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</ControlTheme>
</Grid.Resources>
<ColorSpectrum x:Name="ColorSpectrum1"
Grid.Row="0"
Color="Red"

222
samples/ControlCatalog/Pages/ScrollSnapPage.xaml

@ -1,222 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DesignHeight="800"
d:DesignWidth="400"
x:Class="ControlCatalog.Pages.ScrollSnapPage"
xmlns:pages="using:ControlCatalog.Pages"
x:DataType="pages:ScrollSnapPageViewModel">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock TextWrapping="Wrap"
Classes="h2">Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen.</TextBlock>
<Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto">
<StackPanel Orientation="Horizontal"
Spacing="4">
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Snap Point Type" />
<ComboBox ItemsSource="{Binding AvailableSnapPointsType}"
SelectedItem="{Binding SnapPointsType}" />
</StackPanel>
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Snap Point Alignment" />
<ComboBox ItemsSource="{Binding AvailableSnapPointsAlignment}"
SelectedItem="{Binding SnapPointsAlignment}" />
</StackPanel>
<ToggleSwitch IsChecked="{Binding AreSnapPointsRegular}"
OffContent="No"
OnContent="Yes"
Content="Are Snap Points regular?" />
</StackPanel>
<TextBlock TextWrapping="Wrap"
Grid.Row="1"
Margin="0,10"
Classes="h2">Vertical Snapping</TextBlock>
<Border
BorderBrush="Green"
BorderThickness="1"
Padding="0"
Grid.Row="2"
Margin="10, 5">
<ScrollViewer x:Name="VerticalSnapsScrollViewer"
VerticalSnapPointsType="{Binding SnapPointsType}"
VerticalSnapPointsAlignment="{Binding SnapPointsAlignment}"
HorizontalAlignment="Stretch"
Height="350"
HorizontalScrollBarVisibility="Disabled">
<StackPanel AreVerticalSnapPointsRegular="{Binding AreSnapPointsRegular}"
Orientation="Vertical"
HorizontalAlignment="Stretch">
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 1"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 2"/>
</Border>
<Border Padding="5, 20"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 3"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 4"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 5"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 6"/>
</Border>
<Border Padding="5,8"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 7"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 8"/>
</Border>
<Border Padding="5,4"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 9"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 20"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 11"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 12"/>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
<TextBlock TextWrapping="Wrap"
Grid.Row="3"
Margin="0,10"
Classes="h2">Horizontal Snapping</TextBlock>
<Border
BorderBrush="Green"
BorderThickness="1"
Padding="0"
Grid.Row="4"
Margin="10, 10">
<ScrollViewer x:Name="HorizontalSnapsScrollViewer"
HorizontalSnapPointsType="{Binding SnapPointsType}"
HorizontalSnapPointsAlignment="{Binding SnapPointsAlignment}"
HorizontalAlignment="Stretch"
Height="350"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled">
<StackPanel AreHorizontalSnapPointsRegular="{Binding AreSnapPointsRegular}"
Orientation="Horizontal"
HorizontalAlignment="Stretch">
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 1"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 2"/>
</Border>
<Border Padding="5, 20"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 3"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 4"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 5"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 6"/>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</StackPanel>
</UserControl>

68
samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs

@ -1,68 +0,0 @@
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Markup.Xaml;
using MiniMvvm;
namespace ControlCatalog.Pages
{
public class ScrollSnapPageViewModel : ViewModelBase
{
private SnapPointsType _snapPointsType;
private SnapPointsAlignment _snapPointsAlignment;
private bool _areSnapPointsRegular;
public ScrollSnapPageViewModel()
{
AvailableSnapPointsType = new List<SnapPointsType>()
{
SnapPointsType.None,
SnapPointsType.Mandatory,
SnapPointsType.MandatorySingle
};
AvailableSnapPointsAlignment = new List<SnapPointsAlignment>()
{
SnapPointsAlignment.Near,
SnapPointsAlignment.Center,
SnapPointsAlignment.Far,
};
}
public bool AreSnapPointsRegular
{
get => _areSnapPointsRegular;
set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value);
}
public SnapPointsType SnapPointsType
{
get => _snapPointsType;
set => this.RaiseAndSetIfChanged(ref _snapPointsType, value);
}
public SnapPointsAlignment SnapPointsAlignment
{
get => _snapPointsAlignment;
set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value);
}
public List<SnapPointsType> AvailableSnapPointsType { get; }
public List<SnapPointsAlignment> AvailableSnapPointsAlignment { get; }
}
public class ScrollSnapPage : UserControl
{
public ScrollSnapPage()
{
this.InitializeComponent();
DataContext = new ScrollSnapPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

290
samples/ControlCatalog/Pages/ScrollViewerPage.xaml

@ -3,35 +3,267 @@
xmlns:pages="using:ControlCatalog.Pages"
x:Class="ControlCatalog.Pages.ScrollViewerPage"
x:DataType="pages:ScrollViewerPageViewModel">
<StackPanel Orientation="Vertical" Spacing="20">
<TextBlock TextWrapping="Wrap" Classes="h2">Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling.</TextBlock>
<Grid ColumnDefinitions="Auto, *">
<StackPanel Orientation="Vertical" Spacing="4">
<ToggleSwitch IsChecked="{Binding AllowAutoHide}" Content="Allow auto hide" />
<ToggleSwitch IsChecked="{Binding EnableInertia}" Content="Enable Inertia" />
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Text="Horizontal Scroll" />
<ComboBox ItemsSource="{Binding AvailableVisibility}" SelectedItem="{Binding HorizontalScrollVisibility}" />
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Text="Vertical Scroll" />
<ComboBox ItemsSource="{Binding AvailableVisibility}" SelectedItem="{Binding VerticalScrollVisibility}" />
</StackPanel>
<TabControl>
<TabItem Header="ScrollViewer">
<StackPanel Orientation="Vertical"
Spacing="20">
<TextBlock TextWrapping="Wrap"
Classes="h2">Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling.</TextBlock>
<Grid ColumnDefinitions="Auto, *">
<StackPanel Orientation="Vertical"
Spacing="4">
<ToggleSwitch IsChecked="{Binding AllowAutoHide}"
Content="Allow auto hide" />
<ToggleSwitch IsChecked="{Binding EnableInertia}"
Content="Enable Inertia" />
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Horizontal Scroll" />
<ComboBox ItemsSource="{Binding AvailableVisibility}"
SelectedItem="{Binding HorizontalScrollVisibility}" />
</StackPanel>
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Vertical Scroll" />
<ComboBox ItemsSource="{Binding AvailableVisibility}"
SelectedItem="{Binding VerticalScrollVisibility}" />
</StackPanel>
</StackPanel>
<ScrollViewer x:Name="ScrollViewer"
Grid.Column="1"
Width="400"
Height="400"
IsScrollInertiaEnabled="{Binding EnableInertia}"
AllowAutoHide="{Binding AllowAutoHide}"
HorizontalScrollBarVisibility="{Binding HorizontalScrollVisibility}"
VerticalScrollBarVisibility="{Binding VerticalScrollVisibility}">
<Image Width="800"
Height="800"
Stretch="UniformToFill"
Source="/Assets/delicate-arch-896885_640.jpg" />
</ScrollViewer>
</Grid>
</StackPanel>
</TabItem>
<TabItem Header="Snapping">
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock TextWrapping="Wrap"
Classes="h2">Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen.</TextBlock>
<Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto">
<StackPanel Orientation="Horizontal"
Spacing="4">
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Snap Point Type" />
<ComboBox ItemsSource="{Binding AvailableSnapPointsType}"
SelectedItem="{Binding SnapPointsType}" />
</StackPanel>
<ScrollViewer x:Name="ScrollViewer"
Grid.Column="1"
Width="400" Height="400"
IsScrollInertiaEnabled="{Binding EnableInertia}"
AllowAutoHide="{Binding AllowAutoHide}"
HorizontalScrollBarVisibility="{Binding HorizontalScrollVisibility}"
VerticalScrollBarVisibility="{Binding VerticalScrollVisibility}">
<Image Width="800" Height="800" Stretch="UniformToFill"
Source="/Assets/delicate-arch-896885_640.jpg" />
</ScrollViewer>
</Grid>
</StackPanel>
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock Text="Snap Point Alignment" />
<ComboBox ItemsSource="{Binding AvailableSnapPointsAlignment}"
SelectedItem="{Binding SnapPointsAlignment}" />
</StackPanel>
<ToggleSwitch IsChecked="{Binding AreSnapPointsRegular}"
OffContent="No"
OnContent="Yes"
Content="Are Snap Points regular?" />
</StackPanel>
<TextBlock TextWrapping="Wrap"
Grid.Row="1"
Margin="0,10"
Classes="h2">Vertical Snapping</TextBlock>
<Border
BorderBrush="Green"
BorderThickness="1"
Padding="0"
Grid.Row="2"
Margin="10, 5">
<ScrollViewer x:Name="VerticalSnapsScrollViewer"
VerticalSnapPointsType="{Binding SnapPointsType}"
VerticalSnapPointsAlignment="{Binding SnapPointsAlignment}"
HorizontalAlignment="Stretch"
Height="350"
HorizontalScrollBarVisibility="Disabled">
<StackPanel AreVerticalSnapPointsRegular="{Binding AreSnapPointsRegular}"
Orientation="Vertical"
HorizontalAlignment="Stretch">
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 1"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 2"/>
</Border>
<Border Padding="5, 20"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 3"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 4"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 5"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 6"/>
</Border>
<Border Padding="5,8"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 7"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 8"/>
</Border>
<Border Padding="5,4"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 9"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 20"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 11"/>
</Border>
<Border Padding="5, 30"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
Text="Child 12"/>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
<TextBlock TextWrapping="Wrap"
Grid.Row="3"
Margin="0,10"
Classes="h2">Horizontal Snapping</TextBlock>
<Border
BorderBrush="Green"
BorderThickness="1"
Padding="0"
Grid.Row="4"
Margin="10, 10">
<ScrollViewer x:Name="HorizontalSnapsScrollViewer"
HorizontalSnapPointsType="{Binding SnapPointsType}"
HorizontalSnapPointsAlignment="{Binding SnapPointsAlignment}"
HorizontalAlignment="Stretch"
Height="350"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled">
<StackPanel AreHorizontalSnapPointsRegular="{Binding AreSnapPointsRegular}"
Orientation="Horizontal"
HorizontalAlignment="Stretch">
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
HorizontalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 1"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 2"/>
</Border>
<Border Padding="5, 20"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 3"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 4"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 5"/>
</Border>
<Border Padding="5, 30"
Width="300"
BorderBrush="Red"
VerticalAlignment="Stretch"
BorderThickness="1">
<TextBlock FontWeight="Bold"
VerticalAlignment="Center"
Text="Child 6"/>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</StackPanel>
</TabItem>
</TabControl>
</UserControl>

37
samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs

@ -12,6 +12,9 @@ namespace ControlCatalog.Pages
private bool _enableInertia;
private ScrollBarVisibility _horizontalScrollVisibility;
private ScrollBarVisibility _verticalScrollVisibility;
private SnapPointsType _snapPointsType;
private SnapPointsAlignment _snapPointsAlignment;
private bool _areSnapPointsRegular;
public ScrollViewerPageViewModel()
{
@ -23,6 +26,20 @@ namespace ControlCatalog.Pages
ScrollBarVisibility.Disabled,
};
AvailableSnapPointsType = new List<SnapPointsType>()
{
SnapPointsType.None,
SnapPointsType.Mandatory,
SnapPointsType.MandatorySingle
};
AvailableSnapPointsAlignment = new List<SnapPointsAlignment>()
{
SnapPointsAlignment.Near,
SnapPointsAlignment.Center,
SnapPointsAlignment.Far,
};
HorizontalScrollVisibility = ScrollBarVisibility.Auto;
VerticalScrollVisibility = ScrollBarVisibility.Auto;
AllowAutoHide = true;
@ -54,6 +71,26 @@ namespace ControlCatalog.Pages
}
public List<ScrollBarVisibility> AvailableVisibility { get; }
public bool AreSnapPointsRegular
{
get => _areSnapPointsRegular;
set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value);
}
public SnapPointsType SnapPointsType
{
get => _snapPointsType;
set => this.RaiseAndSetIfChanged(ref _snapPointsType, value);
}
public SnapPointsAlignment SnapPointsAlignment
{
get => _snapPointsAlignment;
set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value);
}
public List<SnapPointsType> AvailableSnapPointsType { get; }
public List<SnapPointsAlignment> AvailableSnapPointsAlignment { get; }
}
public class ScrollViewerPage : UserControl

2
samples/ControlCatalog/Pages/TabControlPage.xaml

@ -51,7 +51,7 @@
Text="From DataTemplate">
</TextBlock>
<TabControl
Items="{Binding Tabs}"
ItemsSource="{Binding Tabs}"
Margin="0 16"
DisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}"
TabStripPlacement="{Binding TabPlacement}">

40
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -308,6 +308,41 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Blur">
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="Effect" Value="blur(0)"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Effect" Value="blur(10)"/>
</KeyFrame>
</Animation>
</Style.Animations>
<Setter Property="Child" Value="{StaticResource Acorn}"/>
</Style>
<Style Selector="Border.DropShadow">
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="Effect" Value="drop-shadow(0 0 0)"/>
</KeyFrame>
<KeyFrame Cue="35%">
<Setter Property="Effect" Value="drop-shadow(5 5 0 Green)"/>
</KeyFrame>
<KeyFrame Cue="70%">
<Setter Property="Effect" Value="drop-shadow(5 5 5 Red)"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Effect" Value="drop-shadow(20 -5 5 Blue)"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>
</UserControl.Styles>
<Grid>
@ -332,6 +367,11 @@
<Border Classes="Test Rect8" Child="{x:Null}" />
<Border Classes="Test Rect9" Child="{x:Null}" />
<Border Classes="Test Rect10" Child="{x:Null}" />
<Border Classes="Test Blur" Background="#ffa0a0a0" BorderThickness="4" BorderBrush="Yellow" Padding="10"/>
<Border Classes="Test DropShadow" Background="Transparent" BorderThickness="4" BorderBrush="Yellow">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center">Drop
Shadow</TextBlock>
</Border>
</WrapPanel>
</StackPanel>
</Grid>

BIN
samples/SafeAreaDemo.Android/Icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

11
samples/SafeAreaDemo.Android/MainActivity.cs

@ -0,0 +1,11 @@
using Android.App;
using Android.Content.PM;
using Avalonia.Android;
namespace SafeAreaDemo.Android
{
[Activity(Label = "SafeAreaDemo.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
public class MainActivity : AvaloniaMainActivity
{
}
}

5
samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" />
<application android:label="SafeAreaDemo" android:icon="@drawable/Icon" />
</manifest>

13
samples/SafeAreaDemo.Android/Resources/drawable/splash_screen.xml

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/splash_background"/>
</item>
<item android:drawable="@drawable/icon"
android:width="120dp"
android:height="120dp"
android:gravity="center" />
</layer-list>

4
samples/SafeAreaDemo.Android/Resources/values/colors.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background">#FFFFFF</color>
</resources>

17
samples/SafeAreaDemo.Android/Resources/values/styles.xml

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="MyTheme">
</style>
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="MyTheme.Splash" parent ="MyTheme.NoActionBar">
<item name="android:windowBackground">@drawable/splash_screen</item>
<item name="android:windowContentOverlay">@null</item>
</style>
</resources>

24
samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0-android</TargetFramework>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
<ApplicationId>com.avalonia.safeareademo</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<AndroidEnableProfiledAot>False</AndroidEnableProfiledAot>
</PropertyGroup>
<ItemGroup>
<AndroidResource Include="Icon.png">
<Link>Resources\drawable\Icon.png</Link>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" />
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" />
</ItemGroup>
</Project>

30
samples/SafeAreaDemo.Android/SplashActivity.cs

@ -0,0 +1,30 @@
using Android.App;
using Android.Content;
using Android.OS;
using Avalonia;
using Avalonia.Android;
using Application = Android.App.Application;
namespace SafeAreaDemo.Android
{
[Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)]
public class SplashActivity : AvaloniaSplashActivity<App>
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder);
}
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
}
protected override void OnResume()
{
base.OnResume();
StartActivity(new Intent(Application.Context, typeof(MainActivity)));
}
}
}

21
samples/SafeAreaDemo.Desktop/Program.cs

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

24
samples/SafeAreaDemo.Desktop/SafeAreaDemo.Desktop.csproj

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects.
One for Windows with net7.0-windows TFM, one for MacOS with net7.0-macos and one with net7.0 TFM for Linux.-->
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" />
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
</Project>

18
samples/SafeAreaDemo.Desktop/app.manifest

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="SafeAreaDemo.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

17
samples/SafeAreaDemo.iOS/AppDelegate.cs

@ -0,0 +1,17 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.iOS;
using Avalonia.Media;
using Foundation;
using UIKit;
namespace SafeAreaDemo.iOS
{
// The UIApplicationDelegate for the application. This class is responsible for launching the
// User Interface of the application, as well as listening (and optionally responding) to
// application events from iOS.
[Register("AppDelegate")]
public partial class AppDelegate : AvaloniaAppDelegate<App>
{
}
}

5
samples/SafeAreaDemo.iOS/Entitlements.plist

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

47
samples/SafeAreaDemo.iOS/Info.plist

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>SafeAreaDemo</string>
<key>CFBundleIdentifier</key>
<string>companyName.SafeAreaDemo</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>10.0</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

15
samples/SafeAreaDemo.iOS/Main.cs

@ -0,0 +1,15 @@
using UIKit;
namespace SafeAreaDemo.iOS
{
public class Application
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}

43
samples/SafeAreaDemo.iOS/Resources/LaunchScreen.xib

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6214" systemVersion="14A314h" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6207" />
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1" />
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" />
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder" />
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480" />
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES" />
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2022 " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines"
minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
<rect key="frame" x="20" y="439" width="441" height="21" />
<fontDescription key="fontDescription" type="system" pointSize="17" />
<color key="textColor" cocoaTouchSystemColor="darkTextColor" />
<nil key="highlightedColor" />
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SafeAreaDemo" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines"
minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
<rect key="frame" x="20" y="140" width="441" height="43" />
<fontDescription key="fontDescription" type="boldSystem" pointSize="36" />
<color key="textColor" cocoaTouchSystemColor="darkTextColor" />
<nil key="highlightedColor" />
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite" />
<constraints>
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC" />
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk" />
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l" />
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0" />
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9" />
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g" />
</constraints>
<nil key="simulatedStatusBarMetrics" />
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics" />
<point key="canvasLocation" x="548" y="455" />
</view>
</objects>
</document>

18
samples/SafeAreaDemo.iOS/SafeAreaDemo.iOS.csproj

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0-ios</TargetFramework>
<SupportedOSPlatformVersion>10.0</SupportedOSPlatformVersion>
<ProvisioningType>manual</ProvisioningType>
<Nullable>enable</Nullable>
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
<!-- These properties need to be set in order to run on a real iDevice -->
<!--<RuntimeIdentifier>ios-arm64</RuntimeIdentifier>-->
<!--<CodesignKey></CodesignKey>-->
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" />
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" />
</ItemGroup>
</Project>

15
samples/SafeAreaDemo/App.xaml

@ -0,0 +1,15 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:SafeAreaDemo"
x:Class="SafeAreaDemo.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

36
samples/SafeAreaDemo/App.xaml.cs

@ -0,0 +1,36 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using SafeAreaDemo.ViewModels;
using SafeAreaDemo.Views;
namespace SafeAreaDemo
{
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainViewModel()
};
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = new MainView
{
DataContext = new MainViewModel()
};
}
base.OnFrameworkInitializationCompleted();
}
}
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

27
samples/SafeAreaDemo/SafeAreaDemo.csproj

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<AvaloniaResource Include="**\*.xaml">
<SubType>Designer</SubType>
</AvaloniaResource>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

31
samples/SafeAreaDemo/ViewLocator.cs

@ -0,0 +1,31 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using MiniMvvm;
namespace SafeAreaDemo
{
public class ViewLocator : IDataTemplate
{
public Control? Build(object? data)
{
if (data is null)
return null;
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = name };
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
}
}

112
samples/SafeAreaDemo/ViewModels/MainViewModel.cs

@ -0,0 +1,112 @@
using Avalonia;
using Avalonia.Controls.Platform;
using MiniMvvm;
namespace SafeAreaDemo.ViewModels
{
public class MainViewModel : ViewModelBase
{
private bool _useSafeArea = true;
private bool _fullscreen;
private IInsetsManager? _insetsManager;
private bool _hideSystemBars;
public Thickness SafeAreaPadding
{
get
{
return _insetsManager?.SafeAreaPadding ?? default;
}
}
public Thickness ViewPadding
{
get
{
return _useSafeArea ? SafeAreaPadding : default;
}
}
public bool UseSafeArea
{
get => _useSafeArea;
set
{
_useSafeArea = value;
this.RaisePropertyChanged();
RaiseSafeAreaChanged();
}
}
public bool Fullscreen
{
get => _fullscreen;
set
{
_fullscreen = value;
if (_insetsManager != null)
{
_insetsManager.DisplayEdgeToEdge = value;
}
this.RaisePropertyChanged();
RaiseSafeAreaChanged();
}
}
public bool HideSystemBars
{
get => _hideSystemBars;
set
{
_hideSystemBars = value;
if (_insetsManager != null)
{
_insetsManager.IsSystemBarVisible = !value;
}
this.RaisePropertyChanged();
RaiseSafeAreaChanged();
}
}
internal IInsetsManager? InsetsManager
{
get => _insetsManager;
set
{
if (_insetsManager != null)
{
_insetsManager.SafeAreaChanged -= InsetsManager_SafeAreaChanged;
}
_insetsManager = value;
if (_insetsManager != null)
{
_insetsManager.SafeAreaChanged += InsetsManager_SafeAreaChanged;
_insetsManager.DisplayEdgeToEdge = _fullscreen;
_insetsManager.IsSystemBarVisible = !_hideSystemBars;
}
}
}
private void InsetsManager_SafeAreaChanged(object? sender, SafeAreaChangedArgs e)
{
RaiseSafeAreaChanged();
}
private void RaiseSafeAreaChanged()
{
this.RaisePropertyChanged(nameof(SafeAreaPadding));
this.RaisePropertyChanged(nameof(ViewPadding));
}
}
}

52
samples/SafeAreaDemo/Views/MainView.xaml

@ -0,0 +1,52 @@
<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"
xmlns:vm="clr-namespace:SafeAreaDemo.ViewModels"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="SafeAreaDemo.Views.MainView"
x:DataType="vm:MainViewModel">
<Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<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">
<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"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Label HorizontalAlignment="Left">Options:</Label>
<CheckBox IsChecked="{Binding Fullscreen}">Fullscreen</CheckBox>
<CheckBox IsChecked="{Binding UseSafeArea}">Use Safe Area</CheckBox>
<CheckBox IsChecked="{Binding HideSystemBars}">Hide System Bars</CheckBox>
<TextBox Width="200" Watermark="Tap to Show Keyboard"/>
</StackPanel>
</Grid>
</DockPanel>
</Border>
</Grid>
</UserControl>

25
samples/SafeAreaDemo/Views/MainView.xaml.cs

@ -0,0 +1,25 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using SafeAreaDemo.ViewModels;
namespace SafeAreaDemo.Views
{
public partial class MainView : UserControl
{
public MainView()
{
AvaloniaXamlLoader.Load(this);
}
protected override void OnLoaded()
{
base.OnLoaded();
var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager;
if (insetsManager != null && DataContext is MainViewModel viewModel)
{
viewModel.InsetsManager = insetsManager;
}
}
}
}

12
samples/SafeAreaDemo/Views/MainWindow.xaml

@ -0,0 +1,12 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:SafeAreaDemo.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
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>

13
samples/SafeAreaDemo/Views/MainWindow.xaml.cs

@ -0,0 +1,13 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace SafeAreaDemo.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
AvaloniaXamlLoader.Load(this);
}
}
}

14
samples/VirtualizationDemo/App.axaml

@ -0,0 +1,14 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="VirtualizationDemo.App">
<Application.Styles>
<FluentTheme/>
</Application.Styles>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

20
samples/VirtualizationDemo/App.axaml.cs

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

7
samples/VirtualizationDemo/App.xaml

@ -1,7 +0,0 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="VirtualizationDemo.App">
<Application.Styles>
<SimpleTheme />
</Application.Styles>
</Application>

21
samples/VirtualizationDemo/App.xaml.cs

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

190
samples/VirtualizationDemo/Assets/chat.json

@ -0,0 +1,190 @@
{
"chat": [
{
"sender": "Alice",
"message": "Hey Bob! How was your weekend?",
"timestamp": "2023-04-01T10:00:00"
},
{
"sender": "Bob",
"message": "It was great, thanks for asking. I went on a camping trip with some friends. How about you?",
"timestamp": "2023-04-01T10:01:00"
},
{
"sender": "Alice",
"message": "My weekend was pretty chill. I just stayed home and caught up on some TV shows.",
"timestamp": "2023-04-01T10:03:00"
},
{
"sender": "Bob",
"message": "That sounds relaxing. What shows did you watch?",
"timestamp": "2023-04-01T10:05:00"
},
{
"sender": "Alice",
"message": "I watched the new season of 'Stranger Things' and started watching 'Ozark'. Have you seen them?",
"timestamp": "2023-04-01T10:07:00"
},
{
"sender": "Bob",
"message": "Yeah, I've seen both of those. They're really good! What do you think of them so far?",
"timestamp": "2023-04-01T10:10:00"
},
{
"sender": "Alice",
"message": "I'm really enjoying 'Stranger Things', but 'Ozark' is a bit darker than I expected. I'm only a few episodes in though, so we'll see how it goes.",
"timestamp": "2023-04-01T10:12:00"
},
{
"sender": "Bob",
"message": "Yeah, 'Ozark' can be intense at times, but it's really well done. Keep watching, it gets even better.",
"timestamp": "2023-04-01T10:15:00"
},
{
"sender": "Alice",
"message": "Thanks for the recommendation, I'll definitely keep watching. So, how's work been for you lately?",
"timestamp": "2023-04-01T10:20:00"
},
{
"sender": "Bob",
"message": "It's been pretty busy, but I'm managing. How about you?",
"timestamp": "2023-04-01T10:22:00"
},
{
"sender": "Alice",
"message": "Same here, things have been pretty hectic. But it keeps us on our toes, right?",
"timestamp": "2023-04-01T10:25:00"
},
{
"sender": "Bob",
"message": "Absolutely. Hey, have you heard about the new project we're starting next week?",
"timestamp": "2023-04-01T10:30:00"
},
{
"sender": "Alice",
"message": "No, I haven't. What's it about?",
"timestamp": "2023-04-01T10:32:00"
},
{
"sender": "Bob",
"message": "It's a big project for a new client, and it's going to require a lot of extra hours from all of us. But the pay is going to be great,so it's definitely worth the extra effort. I'll fill you in on the details later, but for now, let's just enjoy our coffee break, shall we?",
"timestamp": "2023-04-01T10:35:00"
},
{
"sender": "Alice",
"message": "Sounds good to me. I could use a break right about now.",
"timestamp": "2023-04-01T10:40:00"
},
{
"sender": "Bob",
"message": "Me too. So, have you tried the new caf� down the street yet?",
"timestamp": "2023-04-01T10:45:00"
},
{
"sender": "Alice",
"message": "No, I haven't. Is it any good?",
"timestamp": "2023-04-01T10:47:00"
},
{
"sender": "Bob",
"message": "It's really good! They have the best croissants I've ever tasted.",
"timestamp": "2023-04-01T10:50:00"
},
{
"sender": "Alice",
"message": "Hmm, I'll have to try it out sometime. Do they have any vegan options?",
"timestamp": "2023-04-01T10:52:00"
},
{
"sender": "Bob",
"message": "I'm not sure, but I think they do. You should ask them the next time you go there.",
"timestamp": "2023-04-01T10:55:00"
},
{
"sender": "Alice",
"message": "Thanks for the suggestion. I'm always looking for good vegan options around here.",
"timestamp": "2023-04-01T11:00:00"
},
{
"sender": "Bob",
"message": "No problem. So, have you made any plans for the weekend yet?",
"timestamp": "2023-04-01T11:05:00"
},
{
"sender": "Alice",
"message": "Not yet. I was thinking of maybe going for a hike or something. What about you?",
"timestamp": "2023-04-01T11:07:00"
},
{
"sender": "Bob",
"message": "I haven't made any plans either. Maybe we could do something together?",
"timestamp": "2023-04-01T11:10:00"
},
{
"sender": "Alice",
"message": "That sounds like a great idea! Let's plan on it.",
"timestamp": "2023-04-01T11:12:00"
},
{
"sender": "Bob",
"message": "Awesome. I'll check out some hiking trails and let you know which ones look good.",
"timestamp": "2023-04-01T11:15:00"
},
{
"sender": "Alice",
"message": "Sounds good. I can't wait!",
"timestamp": "2023-04-01T11:20:00"
},
{
"sender": "John",
"message": "Hey Lisa, how was your day?",
"timestamp": "2023-04-01T18:00:00"
},
{
"sender": "Lisa",
"message": "It was good, thanks for asking. How about you?",
"timestamp": "2023-04-01T18:05:00"
},
{
"sender": "John",
"message": "Eh, it was alright. Work was pretty busy, but nothing too crazy.",
"timestamp": "2023-04-01T18:10:00"
},
{
"sender": "Lisa",
"message": "Yeah, I know what you mean. My boss has been on my case lately about meeting our deadlines.",
"timestamp": "2023-04-01T18:15:00"
},
{
"sender": "John",
"message": "That sucks. Are you feeling stressed out?",
"timestamp": "2023-04-01T18:20:00"
},
{
"sender": "Lisa",
"message": "A little bit, yeah. But I'm trying to stay positive and focus on getting my work done.",
"timestamp": "2023-04-01T18:25:00"
},
{
"sender": "John",
"message": "That's a good attitude to have. Have you tried doing some meditation or other relaxation techniques?",
"timestamp": "2023-04-01T18:30:00"
},
{
"sender": "Lisa",
"message": "I haven't, but I've been thinking about it. Do you have any suggestions?",
"timestamp": "2023-04-01T18:35:00"
},
{
"sender": "John",
"message": "Sure, I could send you some links to guided meditations that I've found helpful. And there are also some great apps out there that can help you with relaxation.",
"timestamp": "2023-04-01T18:40:00"
},
{
"sender": "Lisa",
"message": "That would be awesome, thanks so much!",
"timestamp": "2023-04-01T18:45:00"
}
]
}

20
samples/VirtualizationDemo/MainWindow.axaml

@ -0,0 +1,20 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:ControlSamples"
xmlns:vm="using:VirtualizationDemo.ViewModels"
xmlns:views="using:VirtualizationDemo.Views"
x:Class="VirtualizationDemo.MainWindow"
Title="AvaloniaUI Virtualization Demo"
x:DataType="vm:MainWindowViewModel">
<controls:HamburgerMenu>
<TabItem Header="Playground" ScrollViewer.VerticalScrollBarVisibility="Disabled">
<views:PlaygroundPageView DataContext="{Binding Playground}"/>
</TabItem>
<TabItem Header="Chat" ScrollViewer.VerticalScrollBarVisibility="Disabled">
<views:ChatPageView DataContext="{Binding Chat}"/>
</TabItem>
<TabItem Header="Expanders" ScrollViewer.VerticalScrollBarVisibility="Disabled">
<views:ExpanderPageView DataContext="{Binding Expanders}"/>
</TabItem>
</controls:HamburgerMenu>
</Window>

15
samples/VirtualizationDemo/MainWindow.axaml.cs

@ -0,0 +1,15 @@
using Avalonia;
using Avalonia.Controls;
using VirtualizationDemo.ViewModels;
namespace VirtualizationDemo;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.AttachDevTools();
DataContext = new MainWindowViewModel();
}
}

64
samples/VirtualizationDemo/MainWindow.xaml

@ -1,64 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="using:VirtualizationDemo.ViewModels"
x:Class="VirtualizationDemo.MainWindow"
Title="AvaloniaUI Virtualization Test"
Width="800"
Height="600"
x:DataType="viewModels:MainWindowViewModel">
<DockPanel LastChildFill="True" Margin="16">
<StackPanel DockPanel.Dock="Right"
Margin="16 0 0 0"
Width="150"
Spacing="4">
<ComboBox Items="{Binding Orientations}"
SelectedItem="{Binding Orientation}"/>
<TextBox Watermark="Item Count"
UseFloatingWatermark="True"
Text="{Binding ItemCount}"/>
<TextBox Watermark="Extent"
UseFloatingWatermark="True"
Text="{Binding #listBox.Scroll.Extent, Mode=OneWay}"/>
<TextBox Watermark="Offset"
UseFloatingWatermark="True"
Text="{Binding #listBox.Scroll.Offset, Mode=OneWay}"/>
<TextBox Watermark="Viewport"
UseFloatingWatermark="True"
Text="{Binding #listBox.Scroll.Viewport, Mode=OneWay}"/>
<TextBlock>Horiz. ScrollBar</TextBlock>
<ComboBox Items="{Binding ScrollBarVisibilities}"
SelectedItem="{Binding HorizontalScrollBarVisibility}"/>
<TextBlock>Vert. ScrollBar</TextBlock>
<ComboBox Items="{Binding ScrollBarVisibilities}"
SelectedItem="{Binding VerticalScrollBarVisibility}"/>
<TextBox Watermark="Item to Create"
UseFloatingWatermark="True"
Text="{Binding NewItemString}"/>
<Button Command="{Binding AddItemCommand}">Add Item</Button>
<Button Command="{Binding RemoveItemCommand}">Remove Item</Button>
<Button Command="{Binding RecreateCommand}">Recreate</Button>
<Button Command="{Binding SelectFirstCommand}">Select First</Button>
<Button Command="{Binding SelectLastCommand}">Select Last</Button>
<Button Command="{Binding RandomizeSize}">Randomize Size</Button>
<Button Command="{Binding ResetSize}">Reset Size</Button>
</StackPanel>
<ListBox Name="listBox"
Items="{Binding Items}"
Selection="{Binding Selection}"
SelectionMode="Multiple"
ScrollViewer.HorizontalScrollBarVisibility="{Binding HorizontalScrollBarVisibility, Mode=TwoWay}"
ScrollViewer.VerticalScrollBarVisibility="{Binding VerticalScrollBarVisibility, Mode=TwoWay}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="{Binding Orientation}"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}" Height="{Binding Height}" TextWrapping="Wrap"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Window>

22
samples/VirtualizationDemo/MainWindow.xaml.cs

@ -1,22 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using VirtualizationDemo.ViewModels;
namespace VirtualizationDemo
{
public class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
this.AttachDevTools();
DataContext = new MainWindowViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

23
samples/VirtualizationDemo/Models/Chat.cs

@ -0,0 +1,23 @@
using System;
using System.IO;
using System.Text.Json;
namespace VirtualizationDemo.Models;
public class ChatFile
{
public ChatMessage[]? Chat { get; set; }
public static ChatFile Load(string path)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
using var s = File.OpenRead(path);
return JsonSerializer.Deserialize<ChatFile>(s, options)!;
}
}
public record ChatMessage(string Sender, string Message, DateTimeOffset Timestamp);

19
samples/VirtualizationDemo/Program.cs

@ -1,15 +1,14 @@
using Avalonia;
namespace VirtualizationDemo
namespace VirtualizationDemo;
class Program
{
class Program
{
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
public static int Main(string[] args)
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
public static int Main(string[] args)
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}

17
samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs

@ -0,0 +1,17 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using VirtualizationDemo.Models;
namespace VirtualizationDemo.ViewModels;
public class ChatPageViewModel
{
public ChatPageViewModel()
{
var chat = ChatFile.Load(Path.Combine("Assets", "chat.json"));
Messages = new(chat.Chat ?? Array.Empty<ChatMessage>());
}
public ObservableCollection<ChatMessage> Messages { get; }
}

21
samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs

@ -0,0 +1,21 @@
using MiniMvvm;
namespace VirtualizationDemo.ViewModels;
public class ExpanderItemViewModel : ViewModelBase
{
private string? _header;
private bool _isExpanded;
public string? Header
{
get => _header;
set => RaiseAndSetIfChanged(ref _header, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
}

17
samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs

@ -0,0 +1,17 @@
using System.Collections.ObjectModel;
using System.Linq;
namespace VirtualizationDemo.ViewModels;
internal class ExpanderPageViewModel
{
public ExpanderPageViewModel()
{
Items = new(Enumerable.Range(0, 100).Select(x => new ExpanderItemViewModel
{
Header = $"Item {x}",
}));
}
public ObservableCollection<ExpanderItemViewModel> Items { get; set; }
}

26
samples/VirtualizationDemo/ViewModels/ItemViewModel.cs

@ -1,26 +0,0 @@
using System;
using MiniMvvm;
namespace VirtualizationDemo.ViewModels
{
internal class ItemViewModel : ViewModelBase
{
private string _prefix;
private int _index;
private double _height = double.NaN;
public ItemViewModel(int index, string prefix = "Item")
{
_prefix = prefix;
_index = index;
}
public string Header => $"{_prefix} {_index}";
public double Height
{
get => _height;
set => this.RaiseAndSetIfChanged(ref _height, value);
}
}
}

164
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@ -1,160 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Controls.Selection;
using MiniMvvm;
using MiniMvvm;
namespace VirtualizationDemo.ViewModels
{
internal class MainWindowViewModel : ViewModelBase
{
private int _itemCount = 200;
private string _newItemString = "New Item";
private int _newItemIndex;
private AvaloniaList<ItemViewModel> _items;
private string _prefix = "Item";
private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto;
private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto;
private Orientation _orientation = Orientation.Vertical;
public MainWindowViewModel()
{
this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems);
RecreateCommand = MiniCommand.Create(() => Recreate());
AddItemCommand = MiniCommand.Create(() => AddItem());
RemoveItemCommand = MiniCommand.Create(() => Remove());
SelectFirstCommand = MiniCommand.Create(() => SelectItem(0));
SelectLastCommand = MiniCommand.Create(() => SelectItem(Items.Count - 1));
}
public string NewItemString
{
get { return _newItemString; }
set { this.RaiseAndSetIfChanged(ref _newItemString, value); }
}
public int ItemCount
{
get { return _itemCount; }
set { this.RaiseAndSetIfChanged(ref _itemCount, value); }
}
public SelectionModel<ItemViewModel> Selection { get; } = new SelectionModel<ItemViewModel>();
public AvaloniaList<ItemViewModel> Items
{
get { return _items; }
private set { this.RaiseAndSetIfChanged(ref _items, value); }
}
public Orientation Orientation
{
get { return _orientation; }
set { this.RaiseAndSetIfChanged(ref _orientation, value); }
}
public IEnumerable<Orientation> Orientations =>
Enum.GetValues(typeof(Orientation)).Cast<Orientation>();
public ScrollBarVisibility HorizontalScrollBarVisibility
{
get { return _horizontalScrollBarVisibility; }
set { this.RaiseAndSetIfChanged(ref _horizontalScrollBarVisibility, value); }
}
namespace VirtualizationDemo.ViewModels;
public ScrollBarVisibility VerticalScrollBarVisibility
{
get { return _verticalScrollBarVisibility; }
set { this.RaiseAndSetIfChanged(ref _verticalScrollBarVisibility, value); }
}
public IEnumerable<ScrollBarVisibility> ScrollBarVisibilities =>
Enum.GetValues(typeof(ScrollBarVisibility)).Cast<ScrollBarVisibility>();
public MiniCommand AddItemCommand { get; private set; }
public MiniCommand RecreateCommand { get; private set; }
public MiniCommand RemoveItemCommand { get; private set; }
public MiniCommand SelectFirstCommand { get; private set; }
public MiniCommand SelectLastCommand { get; private set; }
public void RandomizeSize()
{
var random = new Random();
foreach (var i in Items)
{
i.Height = random.Next(240) + 10;
}
}
public void ResetSize()
{
foreach (var i in Items)
{
i.Height = double.NaN;
}
}
private void ResizeItems(int count)
{
if (Items == null)
{
var items = Enumerable.Range(0, count)
.Select(x => new ItemViewModel(x));
Items = new AvaloniaList<ItemViewModel>(items);
}
else if (count > Items.Count)
{
var items = Enumerable.Range(Items.Count, count - Items.Count)
.Select(x => new ItemViewModel(x));
Items.AddRange(items);
}
else if (count < Items.Count)
{
Items.RemoveRange(count, Items.Count - count);
}
}
private void AddItem()
{
var index = Items.Count;
if (Selection.SelectedItems.Count > 0)
{
index = Selection.SelectedIndex;
}
Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString));
}
private void Remove()
{
if (Selection.SelectedItems.Count > 0)
{
Items.RemoveAll(Selection.SelectedItems.ToList());
}
}
private void Recreate()
{
_prefix = _prefix == "Item" ? "Recreated" : "Item";
var items = Enumerable.Range(0, _itemCount)
.Select(x => new ItemViewModel(x, _prefix));
Items = new AvaloniaList<ItemViewModel>(items);
}
private void SelectItem(int index)
{
Selection.SelectedIndex = index;
}
}
internal class MainWindowViewModel : ViewModelBase
{
public PlaygroundPageViewModel Playground { get; } = new();
public ChatPageViewModel Chat { get; } = new();
public ExpanderPageViewModel Expanders { get; } = new();
}

17
samples/VirtualizationDemo/ViewModels/PlaygroundItemViewModel.cs

@ -0,0 +1,17 @@
using MiniMvvm;
namespace VirtualizationDemo.ViewModels;
public class PlaygroundItemViewModel : ViewModelBase
{
private string? _header;
public PlaygroundItemViewModel(int index) => Header = $"Item {index}";
public PlaygroundItemViewModel(string? header) => Header = header;
public string? Header
{
get => _header;
set => RaiseAndSetIfChanged(ref _header, value);
}
}

95
samples/VirtualizationDemo/ViewModels/PlaygroundPageViewModel.cs

@ -0,0 +1,95 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Selection;
using MiniMvvm;
namespace VirtualizationDemo.ViewModels;
public class PlaygroundPageViewModel : ViewModelBase
{
private SelectionMode _selectionMode = SelectionMode.Multiple;
private int _scrollToIndex = 500;
private string? _newItemHeader = "New Item 1";
public PlaygroundPageViewModel()
{
Items = new(Enumerable.Range(0, 1000).Select(x => new PlaygroundItemViewModel(x)));
Selection = new();
}
public ObservableCollection<PlaygroundItemViewModel> Items { get; }
public bool Multiple
{
get => _selectionMode.HasAnyFlag(SelectionMode.Multiple);
set => SetSelectionMode(SelectionMode.Multiple, value);
}
public bool Toggle
{
get => _selectionMode.HasAnyFlag(SelectionMode.Toggle);
set => SetSelectionMode(SelectionMode.Toggle, value);
}
public bool AlwaysSelected
{
get => _selectionMode.HasAnyFlag(SelectionMode.AlwaysSelected);
set => SetSelectionMode(SelectionMode.AlwaysSelected, value);
}
public SelectionModel<PlaygroundItemViewModel> Selection { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;
set => RaiseAndSetIfChanged(ref _selectionMode, value);
}
public int ScrollToIndex
{
get => _scrollToIndex;
set => RaiseAndSetIfChanged(ref _scrollToIndex, value);
}
public string? NewItemHeader
{
get => _newItemHeader;
set => RaiseAndSetIfChanged(ref _newItemHeader, value);
}
public void ExecuteScrollToIndex()
{
Selection.Select(ScrollToIndex);
}
public void RandomizeScrollToIndex()
{
var rnd = new Random();
ScrollToIndex = rnd.Next(Items.Count);
}
public void AddAtSelectedIndex()
{
if (Selection.SelectedIndex == -1)
return;
Items.Insert(Selection.SelectedIndex, new(NewItemHeader));
}
public void DeleteSelectedItem()
{
var count = Selection.Count;
for (var i = count - 1; i >= 0; i--)
Items.RemoveAt(Selection.SelectedIndexes[i]);
}
private void SetSelectionMode(SelectionMode mode, bool value)
{
if (value)
SelectionMode |= mode;
else
SelectionMode &= ~mode;
}
}

39
samples/VirtualizationDemo/Views/ChatPageView.axaml

@ -0,0 +1,39 @@
<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"
xmlns:vm="using:VirtualizationDemo.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="VirtualizationDemo.Views.ChatPageView"
x:DataType="vm:ChatPageViewModel">
<ListBox ItemsSource="{Binding Messages}">
<ListBox.ItemContainerTheme>
<ControlTheme TargetType="ListBoxItem" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="Padding" Value="8"/>
</ControlTheme>
</ListBox.ItemContainerTheme>
<ListBox.ItemTemplate>
<DataTemplate>
<Border CornerRadius="8"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
TextElement.Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}"
Padding="6"
HorizontalAlignment="Left"
MaxWidth="280">
<DockPanel>
<TextBlock DockPanel.Dock="Top"
Text="{Binding Sender}"
FontWeight="Bold"/>
<TextBlock DockPanel.Dock="Bottom"
Text="{Binding Timestamp}"
FontSize="10"
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"
TextAlignment="Right"
Margin="0 4 0 0"/>
<TextBlock Text="{Binding Message}" TextWrapping="Wrap"/>
</DockPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>

11
samples/VirtualizationDemo/Views/ChatPageView.axaml.cs

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace VirtualizationDemo.Views;
public partial class ChatPageView : UserControl
{
public ChatPageView()
{
InitializeComponent();
}
}

18
samples/VirtualizationDemo/Views/ExpanderPageView.axaml

@ -0,0 +1,18 @@
<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"
xmlns:vm="using:VirtualizationDemo.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="VirtualizationDemo.Views.ExpanderPageView"
x:DataType="vm:ExpanderPageViewModel">
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Header}" IsExpanded="{Binding IsExpanded}">
<Border Width="200" Height="300"/>
</Expander>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>

13
samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace VirtualizationDemo.Views;
public partial class ExpanderPageView : UserControl
{
public ExpanderPageView()
{
InitializeComponent();
}
}

66
samples/VirtualizationDemo/Views/PlaygroundPageView.axaml

@ -0,0 +1,66 @@
<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"
xmlns:vm="using:VirtualizationDemo.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="VirtualizationDemo.Views.PlaygroundPageView"
x:DataType="vm:PlaygroundPageViewModel">
<DockPanel>
<StackPanel DockPanel.Dock="Right" Margin="8 0" Width="200">
<DropDownButton Content="Selection" HorizontalAlignment="Stretch">
<Button.Flyout>
<Flyout>
<StackPanel>
<CheckBox IsChecked="{Binding Multiple}">Multiple</CheckBox>
<CheckBox IsChecked="{Binding Toggle}">Toggle</CheckBox>
<CheckBox IsChecked="{Binding AlwaysSelected}">AlwaysSelected</CheckBox>
<CheckBox IsChecked="{Binding #list.AutoScrollToSelectedItem}">AutoScrollToSelectedItem</CheckBox>
<CheckBox IsChecked="{Binding #list.WrapSelection}">WrapSelection</CheckBox>
</StackPanel>
</Flyout>
</Button.Flyout>
</DropDownButton>
<Label>_Select Item</Label>
<DockPanel>
<TextBox x:Name="scrollToIndex" Text="{Binding ScrollToIndex}">
<TextBox.InnerRightContent>
<StackPanel Orientation="Horizontal">
<Button DockPanel.Dock="Right"
Command="{Binding RandomizeScrollToIndex}"
ToolTip.Tip="Randomize">
&#x27F3;
</Button>
<Button DockPanel.Dock="Right"
Command="{Binding ExecuteScrollToIndex}"
ToolTip.Tip="Execute">
&#11152;
</Button>
</StackPanel>
</TextBox.InnerRightContent>
</TextBox>
</DockPanel>
<Label>New Item</Label>
<TextBox Text="{Binding NewItemHeader}">
<TextBox.InnerRightContent>
<Button Command="{Binding AddAtSelectedIndex}"
ToolTip.Tip="Add at Selected Index">&#x2B;</Button>
</TextBox.InnerRightContent>
</TextBox>
<Button Command="{Binding DeleteSelectedItem}" Margin="0 8 0 0">
Delete Selected
</Button>
</StackPanel>
<TextBlock Name="itemCount" DockPanel.Dock="Bottom"/>
<ListBox Name="list"
ItemsSource="{Binding Items}"
DisplayMemberBinding="{Binding Header}"
Selection="{Binding Selection}"
SelectionMode="{Binding SelectionMode}"/>
</DockPanel>
</UserControl>

44
samples/VirtualizationDemo/Views/PlaygroundPageView.axaml.cs

@ -0,0 +1,44 @@
using System;
using System.Linq;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
namespace VirtualizationDemo.Views;
public partial class PlaygroundPageView : UserControl
{
private DispatcherTimer _timer;
public PlaygroundPageView()
{
InitializeComponent();
_timer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(500),
};
_timer.Tick += TimerTick;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_timer.Start();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_timer.Stop();
}
private void TimerTick(object? sender, EventArgs e)
{
var message = $"Realized {list.GetRealizedContainers().Count()} of {list.ItemsPanelRoot?.Children.Count}";
itemCount.Text = message;
}
}

21
samples/VirtualizationDemo/VirtualizationDemo.csproj

@ -1,19 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
<ProjectReference Include="..\SampleControls\ControlSamples.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Assets\chat.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\Rx.props" />
<Import Condition="'$(TargetFramework)'=='net461'" Project="..\..\build\NetFX.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\SourceGenerators.props" />
<Import Project="..\..\build\NullableEnable.props" />
</Project>

6
src/Android/Avalonia.Android/AndroidInputMethod.cs

@ -1,14 +1,11 @@
using System;
using Android.Content;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Input.TextInput;
using Avalonia.Reactive;
namespace Avalonia.Android
{
@ -99,6 +96,9 @@ namespace Avalonia.Android
{
_host.InitEditorInfo((topLevel, outAttrs) =>
{
if (_client == null)
return null;
_inputConnection = new AvaloniaInputConnection(topLevel, this);
outAttrs.InputType = options.ContentType switch

1
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -38,7 +38,6 @@ namespace Avalonia.Android
Options = AvaloniaLocator.Current.GetService<AndroidPlatformOptions>() ?? new AndroidPlatformOptions();
AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToTransient<ClipboardImpl>()
.Bind<ICursorFactory>().ToTransient<CursorFactory>()
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()

2
src/Android/Avalonia.Android/AndroidThreadingInterface.cs

@ -21,8 +21,6 @@ namespace Avalonia.Android
_handler = new Handler(App.Context.MainLooper);
}
public void RunLoop(CancellationToken cancellationToken) => throw new NotSupportedException();
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
if (interval.TotalMilliseconds < 10)

4
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@ -32,10 +32,6 @@ namespace Avalonia.Android
{
lifetime.View = View;
}
Window?.ClearFlags(WindowManagerFlags.TranslucentStatus);
Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
base.OnCreate(savedInstanceState);
SetContentView(View);

49
src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs

@ -2,11 +2,10 @@
using System.Collections.Generic;
using Android.OS;
using Android.Views;
using AndroidX.AppCompat.App;
using AndroidX.Core.View;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls.Platform;
using static Avalonia.Controls.Platform.IInsetsManager;
using Avalonia.Media;
namespace Avalonia.Android.Platform
{
@ -20,6 +19,7 @@ namespace Avalonia.Android.Platform
private bool? _systemUiVisibility;
private SystemBarTheme? _statusBarTheme;
private bool? _isDefaultSystemBarLightTheme;
private Color? _systemBarColor;
public event EventHandler<SafeAreaChangedArgs> SafeAreaChanged;
@ -36,6 +36,16 @@ namespace Avalonia.Android.Platform
}
WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value);
if(value)
{
_activity.Window.AddFlags(WindowManagerFlags.TranslucentStatus);
_activity.Window.AddFlags(WindowManagerFlags.TranslucentNavigation);
}
else
{
SystemBarColor = _systemBarColor;
}
}
}
@ -71,7 +81,7 @@ namespace Avalonia.Android.Platform
var renderScaling = _topLevel.RenderScaling;
var inset = insets.GetInsets(
(DisplayEdgeToEdge ?
(_displayEdgeToEdge ?
WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() |
WindowInsetsCompat.Type.DisplayCutout() :
0) | WindowInsetsCompat.Type.Ime());
@ -81,8 +91,8 @@ namespace Avalonia.Android.Platform
return new Thickness(inset.Left / renderScaling,
inset.Top / renderScaling,
inset.Right / renderScaling,
(imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ?
imeInset.Bottom - navBarInset.Bottom :
(imeInset.Bottom > 0 && ((_usesLegacyLayouts && !_displayEdgeToEdge) || !_usesLegacyLayouts) ?
imeInset.Bottom - (_displayEdgeToEdge ? 0 : navBarInset.Bottom) :
inset.Bottom) / renderScaling);
}
@ -93,6 +103,7 @@ namespace Avalonia.Android.Platform
public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets)
{
NotifySafeAreaChanged(SafeAreaPadding);
insets = ViewCompat.OnApplyWindowInsets(v, insets);
return insets;
}
@ -146,8 +157,6 @@ namespace Avalonia.Android.Platform
compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light;
compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light;
AppCompatDelegate.DefaultNightMode = isDefault ? AppCompatDelegate.ModeNightFollowSystem : compat.AppearanceLightStatusBars ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes;
}
}
@ -190,10 +199,36 @@ namespace Avalonia.Android.Platform
}
}
public Color? SystemBarColor
{
get => _systemBarColor;
set
{
_systemBarColor = value;
if (_systemBarColor is { } color && !_displayEdgeToEdge && _activity.Window != null)
{
_activity.Window.ClearFlags(WindowManagerFlags.TranslucentStatus);
_activity.Window.ClearFlags(WindowManagerFlags.TranslucentNavigation);
_activity.Window.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
var androidColor = global::Android.Graphics.Color.Argb(color.A, color.R, color.G, color.B);
_activity.Window.SetStatusBarColor(androidColor);
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
// As we can only change the navigation bar's foreground api 26 and newer, we only change the background color if running on those versions
_activity.Window.SetNavigationBarColor(androidColor);
}
}
}
}
internal void ApplyStatusBarState()
{
IsSystemBarVisible = _systemUiVisibility;
SystemBarTheme = _statusBarTheme;
SystemBarColor = _systemBarColor;
}
private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback

30
src/Android/Avalonia.Android/Platform/ClipboardImpl.cs

@ -2,32 +2,26 @@ using System;
using System.Threading.Tasks;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Platform;
namespace Avalonia.Android.Platform
{
internal class ClipboardImpl : IClipboard
{
private Context context = (AvaloniaLocator.Current.GetService<IWindowImpl>() as View).Context;
private ClipboardManager? _clipboardManager;
private ClipboardManager ClipboardManager
internal ClipboardImpl(ClipboardManager? value)
{
get
{
return this.context.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>();
}
_clipboardManager = value;
}
public Task<string> GetTextAsync()
{
if (ClipboardManager.HasPrimaryClip)
if (_clipboardManager?.HasPrimaryClip == true)
{
return Task.FromResult<string>(ClipboardManager.PrimaryClip.GetItemAt(0).Text);
return Task.FromResult<string>(_clipboardManager.PrimaryClip.GetItemAt(0).Text);
}
return Task.FromResult<string>(null);
@ -35,15 +29,25 @@ namespace Avalonia.Android.Platform
public Task SetTextAsync(string text)
{
if(_clipboardManager == null)
{
return Task.CompletedTask;
}
ClipData clip = ClipData.NewPlainText("text", text);
ClipboardManager.PrimaryClip = clip;
_clipboardManager.PrimaryClip = clip;
return Task.FromResult<object>(null);
}
public Task ClearAsync()
{
ClipboardManager.PrimaryClip = null;
if (_clipboardManager == null)
{
return Task.CompletedTask;
}
_clipboardManager.PrimaryClip = null;
return Task.FromResult<object>(null);
}

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

@ -3,9 +3,13 @@ using System.Collections.Generic;
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using AndroidX.AppCompat.App;
using Avalonia.Android.Platform.Specific;
using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Android.Platform.Storage;
@ -13,6 +17,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.OpenGL.Egl;
@ -22,13 +27,7 @@ using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Java.Lang;
using Java.Util;
using Math = System.Math;
using AndroidRect = Android.Graphics.Rect;
using Window = Android.Views.Window;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Text;
using ClipboardManager = Android.Content.ClipboardManager;
namespace Avalonia.Android.Platform.SkiaPlatform
{
@ -44,6 +43,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly IStorageProvider _storageProvider;
private readonly ISystemNavigationManagerImpl _systemNavigationManager;
private readonly AndroidInsetsManager _insetsManager;
private readonly ClipboardImpl _clipboard;
private ViewImpl _view;
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@ -54,6 +54,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_pointerHelper = new AndroidMotionEventsHelper(this);
_gl = new EglGlPlatformSurface(this);
_framebuffer = new FramebufferManager(this);
_clipboard = new ClipboardImpl(avaloniaView.Context?.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>());
RenderScaling = _view.Scaling;
@ -286,6 +287,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_ => null,
};
}
AppCompatDelegate.DefaultNightMode = themeVariant == PlatformThemeVariant.Light ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes;
}
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
@ -408,6 +411,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return _insetsManager;
}
if(featureType == typeof(IClipboard))
{
return _clipboard;
}
return null;
}
}

41
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -1,6 +1,6 @@
using System;
using Avalonia.Reactive;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia
{
@ -34,8 +34,8 @@ namespace Avalonia
/// </remarks>
public static IObservable<object?> GetObservable(this AvaloniaObject o, AvaloniaProperty property)
{
return new AvaloniaPropertyObservable<object?>(
o ?? throw new ArgumentNullException(nameof(o)),
return new AvaloniaPropertyObservable<object?, object?>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)));
}
@ -54,11 +54,23 @@ namespace Avalonia
/// </remarks>
public static IObservable<T> GetObservable<T>(this AvaloniaObject o, AvaloniaProperty<T> property)
{
return new AvaloniaPropertyObservable<T>(
return new AvaloniaPropertyObservable<T, T>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)));
}
/// <inheritdoc cref="GetObservable{T}(AvaloniaObject, AvaloniaProperty{T})"/>
/// <param name="o"/>
/// <param name="property"/>
/// <param name="converter">A method which is executed to convert each property value to <typeparamref name="TResult"/>.</param>
public static IObservable<TResult> GetObservable<TSource, TResult>(this AvaloniaObject o, AvaloniaProperty<TSource> property, Func<TSource, TResult> converter)
{
return new AvaloniaPropertyObservable<TSource, TResult>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)),
converter ?? throw new ArgumentNullException(nameof(converter)));
}
/// <summary>
/// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary>
@ -75,7 +87,7 @@ namespace Avalonia
this AvaloniaObject o,
AvaloniaProperty property)
{
return new AvaloniaPropertyBindingObservable<object?>(
return new AvaloniaPropertyBindingObservable<object?, object?>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)));
}
@ -97,12 +109,27 @@ namespace Avalonia
this AvaloniaObject o,
AvaloniaProperty<T> property)
{
return new AvaloniaPropertyBindingObservable<T>(
return new AvaloniaPropertyBindingObservable<T, T>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)));
}
/// <inheritdoc cref="GetBindingObservable{T}(AvaloniaObject, AvaloniaProperty{T})"/>
/// <param name="o"/>
/// <param name="property"/>
/// <param name="converter">A method which is executed to convert each property value to <typeparamref name="TResult"/>.</param>
public static IObservable<BindingValue<TResult>> GetBindingObservable<TSource, TResult>(
this AvaloniaObject o,
AvaloniaProperty<TSource> property,
Func<TSource, TResult> converter)
{
return new AvaloniaPropertyBindingObservable<TSource, TResult>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)),
converter ?? throw new ArgumentNullException(nameof(converter)));
}
/// <summary>
/// Gets an observable that listens for property changed events for an
/// <see cref="AvaloniaProperty"/>.
@ -338,7 +365,7 @@ namespace Avalonia
return InstancedBinding.OneWay(_source);
}
}
private class ClassHandlerObserver<TTarget, TValue> : IObserver<AvaloniaPropertyChangedEventArgs<TValue>>
{
private readonly Action<TTarget, AvaloniaPropertyChangedEventArgs<TValue>> _action;

6
src/Avalonia.Base/AvaloniaProperty.cs

@ -499,6 +499,12 @@ namespace Avalonia
/// <param name="o">The object instance.</param>
internal abstract void RouteClearValue(AvaloniaObject o);
/// <summary>
/// Routes an untyped CoerceValue call on a property with its default value to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
internal abstract void RouteCoerceDefaultValue(AvaloniaObject o);
/// <summary>
/// Routes an untyped GetValue call to a typed call.
/// </summary>

14
src/Avalonia.Base/CombinedGeometry.cs

@ -152,19 +152,15 @@ namespace Avalonia.Media
var g1 = Geometry1;
var g2 = Geometry2;
if (g1 is object && g2 is object)
if (g1?.PlatformImpl != null && g2?.PlatformImpl != null)
{
var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
return factory.CreateCombinedGeometry(GeometryCombineMode, g1, g2);
return factory.CreateCombinedGeometry(GeometryCombineMode, g1.PlatformImpl, g2.PlatformImpl);
}
else if (GeometryCombineMode == GeometryCombineMode.Intersect)
return null;
else if (g1 is object)
return g1.PlatformImpl;
else if (g2 is object)
return g2.PlatformImpl;
else
if (GeometryCombineMode == GeometryCombineMode.Intersect)
return null;
return g1?.PlatformImpl ?? g2?.PlatformImpl;
}
}
}

26
src/Avalonia.Base/Controls/ResourceDictionary.cs

@ -15,6 +15,7 @@ namespace Avalonia.Controls
/// </summary>
public class ResourceDictionary : IResourceDictionary
{
private object? lastDeferredItemKey;
private Dictionary<object, object?>? _inner;
private IResourceHost? _owner;
private AvaloniaList<IResourceProvider>? _mergedDictionaries;
@ -241,12 +242,27 @@ namespace Avalonia.Controls
{
if (value is DeferredItem deffered)
{
_inner[key] = value = deffered.Factory(null) switch
// Avoid simple reentrancy, which could commonly occur on redefining the resource.
if (lastDeferredItemKey == key)
{
ITemplateResult t => t.Result,
object v => v,
_ => null,
};
value = null;
return false;
}
try
{
lastDeferredItemKey = key;
_inner[key] = value = deffered.Factory(null) switch
{
ITemplateResult t => t.Result,
{ } v => v,
_ => null,
};
}
finally
{
lastDeferredItemKey = null;
}
}
return true;
}

2
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

@ -40,7 +40,7 @@ namespace Avalonia.Data.Core
{
if (reference.TryGetTarget(out var target) && target is AvaloniaObject obj)
{
_subscription = new AvaloniaPropertyObservable<object?>(obj, _property).Subscribe(ValueChanged);
_subscription = new AvaloniaPropertyObservable<object?,object?>(obj, _property).Subscribe(ValueChanged);
}
else
{

5
src/Avalonia.Base/DirectPropertyBase.cs

@ -117,6 +117,11 @@ namespace Avalonia
o.ClearValue<TValue>(this);
}
internal override void RouteCoerceDefaultValue(AvaloniaObject o)
{
// Do nothing.
}
/// <inheritdoc/>
internal override object? RouteGetValue(AvaloniaObject o)
{

53
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -4,14 +4,17 @@ using Avalonia.Threading;
namespace Avalonia.Input.GestureRecognizers
{
public class ScrollGestureRecognizer
: StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise
IGestureRecognizer
public class ScrollGestureRecognizer : AvaloniaObject, IGestureRecognizer
{
// Pixels per second speed that is considered to be the stop of inertial scroll
internal const double InertialScrollSpeedEnd = 5;
public const double InertialResistance = 0.15;
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private bool _isScrollInertiaEnabled;
private int _scrollStartDistance = 30;
private bool _scrolling;
private Point _trackedRootPoint;
private IPointer? _tracking;
@ -28,34 +31,39 @@ namespace Avalonia.Input.GestureRecognizers
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanHorizontallyScrollProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll));
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll),
o => o.CanHorizontallyScroll, (o, v) => o.CanHorizontallyScroll = v);
/// <summary>
/// Defines the <see cref="CanVerticallyScroll"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanVerticallyScrollProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(CanVerticallyScroll));
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanVerticallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(CanVerticallyScroll),
o => o.CanVerticallyScroll, (o, v) => o.CanVerticallyScroll = v);
/// <summary>
/// Defines the <see cref="IsScrollInertiaEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsScrollInertiaEnabledProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(IsScrollInertiaEnabled));
public static readonly DirectProperty<ScrollGestureRecognizer, bool> IsScrollInertiaEnabledProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(IsScrollInertiaEnabled),
o => o.IsScrollInertiaEnabled, (o,v) => o.IsScrollInertiaEnabled = v);
/// <summary>
/// Defines the <see cref="ScrollStartDistance"/> property.
/// </summary>
public static readonly StyledProperty<int> ScrollStartDistanceProperty =
AvaloniaProperty.Register<ScrollGestureRecognizer, int>(nameof(ScrollStartDistance), 30);
public static readonly DirectProperty<ScrollGestureRecognizer, int> ScrollStartDistanceProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, int>(nameof(ScrollStartDistance),
o => o.ScrollStartDistance, (o, v) => o.ScrollStartDistance = v,
unsetValue: 30);
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
public bool CanHorizontallyScroll
{
get => GetValue(CanHorizontallyScrollProperty);
set => SetValue(CanHorizontallyScrollProperty, value);
get => _canHorizontallyScroll;
set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value);
}
/// <summary>
@ -63,17 +71,17 @@ namespace Avalonia.Input.GestureRecognizers
/// </summary>
public bool CanVerticallyScroll
{
get => GetValue(CanVerticallyScrollProperty);
set => SetValue(CanVerticallyScrollProperty, value);
get => _canVerticallyScroll;
set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
}
/// <summary>
/// Gets or sets whether the gesture should include inertia in it's behavior.
/// </summary>
public bool IsScrollInertiaEnabled
{
get => GetValue(IsScrollInertiaEnabledProperty);
set => SetValue(IsScrollInertiaEnabledProperty, value);
get => _isScrollInertiaEnabled;
set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value);
}
/// <summary>
@ -81,10 +89,9 @@ namespace Avalonia.Input.GestureRecognizers
/// </summary>
public int ScrollStartDistance
{
get => GetValue(ScrollStartDistanceProperty);
set => SetValue(ScrollStartDistanceProperty, value);
}
get => _scrollStartDistance;
set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value);
}
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
{

24
src/Avalonia.Base/Layout/LayoutManager.cs

@ -269,21 +269,25 @@ namespace Avalonia.Layout
}
}
private void Measure(Layoutable control)
private bool Measure(Layoutable control)
{
if (!control.IsVisible || !control.IsAttachedToVisualTree)
return false;
// Controls closest to the visual root need to be arranged first. We don't try to store
// ordered invalidation lists, instead we traverse the tree upwards, measuring the
// controls closest to the root first. This has been shown by benchmarks to be the
// fastest and most memory-efficient algorithm.
if (control.VisualParent is Layoutable parent)
{
Measure(parent);
if (!Measure(parent))
return false;
}
// If the control being measured has IsMeasureValid == true here then its measure was
// handed by an ancestor and can be ignored. The measure may have also caused the
// control to be removed.
if (!control.IsMeasureValid && control.IsAttachedToVisualTree)
if (!control.IsMeasureValid)
{
if (control is ILayoutRoot root)
{
@ -294,16 +298,22 @@ namespace Avalonia.Layout
control.Measure(control.PreviousMeasure.Value);
}
}
return true;
}
private void Arrange(Layoutable control)
private bool Arrange(Layoutable control)
{
if (!control.IsVisible || !control.IsAttachedToVisualTree)
return false;
if (control.VisualParent is Layoutable parent)
{
Arrange(parent);
if (!Arrange(parent))
return false;
}
if (!control.IsArrangeValid && control.IsAttachedToVisualTree)
if (control.IsMeasureValid && !control.IsArrangeValid)
{
if (control is IEmbeddedLayoutRoot embeddedRoot)
control.Arrange(new Rect(embeddedRoot.AllocatedSize));
@ -316,6 +326,8 @@ namespace Avalonia.Layout
control.Arrange(control.PreviousArrange.Value);
}
}
return true;
}
private void QueueLayoutPass()

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

@ -125,7 +125,7 @@ namespace Avalonia.Layout
AvaloniaProperty.Register<Layoutable, VerticalAlignment>(nameof(VerticalAlignment));
/// <summary>
/// Defines the <see cref="UseLayoutRoundingProperty"/> property.
/// Defines the <see cref="UseLayoutRounding"/> property.
/// </summary>
public static readonly StyledProperty<bool> UseLayoutRoundingProperty =
AvaloniaProperty.Register<Layoutable, bool>(nameof(UseLayoutRounding), defaultValue: true, inherits: true);

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

@ -309,7 +309,7 @@ namespace Avalonia.Media
if (input.Length == 3 || input.Length == 4)
{
var extendedLength = 2 * input.Length;
#if !BUILDTASK
Span<char> extended = stackalloc char[extendedLength];
#else

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

@ -132,7 +132,7 @@ namespace Avalonia.Media
double radiusX = 0, double radiusY = 0,
BoxShadows boxShadows = default)
{
if (brush == null && !PenIsVisible(pen))
if (brush == null && !PenIsVisible(pen) && boxShadows.Count == 0)
return;
if (!MathUtilities.IsZero(radiusX))
{
@ -160,7 +160,7 @@ namespace Avalonia.Media
/// </remarks>
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default)
{
if (brush == null && !PenIsVisible(pen))
if (brush == null && !PenIsVisible(pen) && boxShadows.Count == 0)
return;
DrawRectangleCore(brush, pen, rrect, boxShadows);
}

22
src/Avalonia.Base/Media/Effects/BlurEffect.cs

@ -0,0 +1,22 @@
using System;
// ReSharper disable CheckNamespace
namespace Avalonia.Media;
public class BlurEffect : Effect, IBlurEffect, IMutableEffect
{
public static readonly StyledProperty<double> RadiusProperty = AvaloniaProperty.Register<BlurEffect, double>(
nameof(Radius), 5);
public double Radius
{
get => GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
static BlurEffect()
{
AffectsRender<BlurEffect>(RadiusProperty);
}
public IImmutableEffect ToImmutable() => new ImmutableBlurEffect(Radius);
}

104
src/Avalonia.Base/Media/Effects/DropShadowEffect.cs

@ -0,0 +1,104 @@
// ReSharper disable once CheckNamespace
using System;
// ReSharper disable CheckNamespace
namespace Avalonia.Media;
public abstract class DropShadowEffectBase : Effect
{
public static readonly StyledProperty<double> BlurRadiusProperty =
AvaloniaProperty.Register<DropShadowEffectBase, double>(
nameof(BlurRadius), 5);
public double BlurRadius
{
get => GetValue(BlurRadiusProperty);
set => SetValue(BlurRadiusProperty, value);
}
public static readonly StyledProperty<Color> ColorProperty = AvaloniaProperty.Register<DropShadowEffectBase, Color>(
nameof(Color), Colors.Black);
public Color Color
{
get => GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
public static readonly StyledProperty<double> OpacityProperty =
AvaloniaProperty.Register<DropShadowEffectBase, double>(
nameof(Opacity), 1);
public double Opacity
{
get => GetValue(OpacityProperty);
set => SetValue(OpacityProperty, value);
}
static DropShadowEffectBase()
{
AffectsRender<DropShadowEffectBase>(BlurRadiusProperty, ColorProperty, OpacityProperty);
}
}
public class DropShadowEffect : DropShadowEffectBase, IDropShadowEffect, IMutableEffect
{
public static readonly StyledProperty<double> OffsetXProperty = AvaloniaProperty.Register<DropShadowEffect, double>(
nameof(OffsetX), 3.5355);
public double OffsetX
{
get => GetValue(OffsetXProperty);
set => SetValue(OffsetXProperty, value);
}
public static readonly StyledProperty<double> OffsetYProperty = AvaloniaProperty.Register<DropShadowEffect, double>(
nameof(OffsetY), 3.5355);
public double OffsetY
{
get => GetValue(OffsetYProperty);
set => SetValue(OffsetYProperty, value);
}
static DropShadowEffect()
{
AffectsRender<DropShadowEffect>(OffsetXProperty, OffsetYProperty);
}
public IImmutableEffect ToImmutable()
{
return new ImmutableDropShadowEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity);
}
}
/// <summary>
/// This class is compatible with WPF's DropShadowEffect and provides Direction and ShadowDepth properties instead of OffsetX/OffsetY
/// </summary>
public class DropShadowDirectionEffect : DropShadowEffectBase, IDirectionDropShadowEffect, IMutableEffect
{
public static readonly StyledProperty<double> ShadowDepthProperty =
AvaloniaProperty.Register<DropShadowDirectionEffect, double>(
nameof(ShadowDepth), 5);
public double ShadowDepth
{
get => GetValue(ShadowDepthProperty);
set => SetValue(ShadowDepthProperty, value);
}
public static readonly StyledProperty<double> DirectionProperty = AvaloniaProperty.Register<DropShadowDirectionEffect, double>(
nameof(Direction), 315);
public double Direction
{
get => GetValue(DirectionProperty);
set => SetValue(DirectionProperty, value);
}
public double OffsetX => Math.Cos(Direction * Math.PI / 180) * ShadowDepth;
public double OffsetY => Math.Sin(Direction * Math.PI / 180) * ShadowDepth;
public IImmutableEffect ToImmutable() => new ImmutableDropShadowDirectionEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity);
}

93
src/Avalonia.Base/Media/Effects/Effect.cs

@ -0,0 +1,93 @@
using System;
using Avalonia.Animation;
using Avalonia.Animation.Animators;
using Avalonia.Reactive;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Utilities;
// ReSharper disable once CheckNamespace
namespace Avalonia.Media;
public class Effect : Animatable, IAffectsRender
{
/// <summary>
/// Marks a property as affecting the brush's visual representation.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a brush's static constructor, any change to the
/// property will cause the <see cref="Invalidated"/> event to be raised on the brush.
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : Effect
{
var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty));
foreach (var property in properties)
{
property.Changed.Subscribe(invalidateObserver);
}
}
/// <summary>
/// Raises the <see cref="Invalidated"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e);
/// <inheritdoc />
public event EventHandler? Invalidated;
static Exception ParseError(string s) => throw new ArgumentException("Unable to parse effect: " + s);
public static IEffect Parse(string s)
{
var span = s.AsSpan();
var r = new TokenParser(span);
if (r.TryConsume("blur"))
{
if (!r.TryConsume('(') || !r.TryParseDouble(out var radius) || !r.TryConsume(')') || !r.IsEofWithWhitespace())
throw ParseError(s);
return new ImmutableBlurEffect(radius);
}
if (r.TryConsume("drop-shadow"))
{
if (!r.TryConsume('(') || !r.TryParseDouble(out var offsetX)
|| !r.TryParseDouble(out var offsetY))
throw ParseError(s);
double blurRadius = 0;
var color = Colors.Black;
if (!r.TryConsume(')'))
{
if (!r.TryParseDouble(out blurRadius) || blurRadius < 0)
throw ParseError(s);
if (!r.TryConsume(')'))
{
var endOfExpression = s.LastIndexOf(")", StringComparison.Ordinal);
if (endOfExpression == -1)
throw ParseError(s);
if (!new TokenParser(span.Slice(endOfExpression + 1)).IsEofWithWhitespace())
throw ParseError(s);
if (!Color.TryParse(span.Slice(r.Position, endOfExpression - r.Position).TrimEnd(), out color))
throw ParseError(s);
return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1);
}
}
if (!r.IsEofWithWhitespace())
throw ParseError(s);
return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1);
}
throw ParseError(s);
}
static Effect()
{
EffectAnimator.EnsureRegistered();
}
}

131
src/Avalonia.Base/Media/Effects/EffectAnimator.cs

@ -0,0 +1,131 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Media;
// ReSharper disable once CheckNamespace
namespace Avalonia.Animation.Animators;
public class EffectAnimator : Animator<IEffect?>
{
public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock,
IObservable<bool> match, Action? onComplete)
{
if (TryCreateAnimator<BlurEffectAnimator, IBlurEffect>(out var animator)
|| TryCreateAnimator<DropShadowEffectAnimator, IDropShadowEffect>(out animator))
return animator.Apply(animation, control, clock, match, onComplete);
Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log(
this,
"The animation's keyframe value types set is not supported.");
return base.Apply(animation, control, clock, match, onComplete);
}
private bool TryCreateAnimator<TAnimator, TInterface>([NotNullWhen(true)] out IAnimator? animator)
where TAnimator : EffectAnimatorBase<TInterface>, new() where TInterface : class, IEffect
{
TAnimator? createdAnimator = null;
foreach (var keyFrame in this)
{
if (keyFrame.Value is TInterface)
{
createdAnimator ??= new TAnimator()
{
Property = Property
};
createdAnimator.Add(new AnimatorKeyFrame(typeof(TAnimator), () => new TAnimator(), keyFrame.Cue,
keyFrame.KeySpline)
{
Value = keyFrame.Value
});
}
else
{
animator = null;
return false;
}
}
animator = createdAnimator;
return animator != null;
}
/// <summary>
/// Fallback implementation of <see cref="IEffect"/> animation.
/// </summary>
public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue) => progress >= 0.5 ? newValue : oldValue;
private static bool s_Registered;
public static void EnsureRegistered()
{
if(s_Registered)
return;
s_Registered = true;
Animation.RegisterAnimator<EffectAnimator>(prop =>
typeof(IEffect).IsAssignableFrom(prop.PropertyType));
}
}
public abstract class EffectAnimatorBase<T> : Animator<IEffect?> where T : class, IEffect?
{
public override IDisposable BindAnimation(Animatable control, IObservable<IEffect?> instance)
{
if (Property is null)
{
throw new InvalidOperationException("Animator has no property specified.");
}
return control.Bind((AvaloniaProperty<IEffect?>)Property, instance, BindingPriority.Animation);
}
protected abstract T Interpolate(double progress, T oldValue, T newValue);
public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue)
{
var old = oldValue as T;
var n = newValue as T;
if (old == null || n == null)
return progress >= 0.5 ? newValue : oldValue;
return Interpolate(progress, old, n);
}
}
public class BlurEffectAnimator : EffectAnimatorBase<IBlurEffect>
{
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
protected override IBlurEffect Interpolate(double progress, IBlurEffect oldValue, IBlurEffect newValue)
{
return new ImmutableBlurEffect(
s_doubleAnimator.Interpolate(progress, oldValue.Radius, newValue.Radius));
}
}
public class DropShadowEffectAnimator : EffectAnimatorBase<IDropShadowEffect>
{
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
protected override IDropShadowEffect Interpolate(double progress, IDropShadowEffect oldValue,
IDropShadowEffect newValue)
{
var blur = s_doubleAnimator.Interpolate(progress, oldValue.BlurRadius, newValue.BlurRadius);
var color = ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color);
var opacity = s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity);
if (oldValue is IDirectionDropShadowEffect oldDirection && newValue is IDirectionDropShadowEffect newDirection)
{
return new ImmutableDropShadowDirectionEffect(
s_doubleAnimator.Interpolate(progress, oldDirection.Direction, newDirection.Direction),
s_doubleAnimator.Interpolate(progress, oldDirection.ShadowDepth, newDirection.ShadowDepth),
blur, color, opacity
);
}
return new ImmutableDropShadowEffect(
s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX),
s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY),
blur, color, opacity
);
}
}

18
src/Avalonia.Base/Media/Effects/EffectConverter.cs

@ -0,0 +1,18 @@
using System;
using System.ComponentModel;
using System.Globalization;
namespace Avalonia.Media;
public class EffectConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value)
{
return value is string s ? Effect.Parse(s) : null;
}
}

56
src/Avalonia.Base/Media/Effects/EffectExtesions.cs

@ -0,0 +1,56 @@
using System;
// ReSharper disable once CheckNamespace
namespace Avalonia.Media;
public static class EffectExtensions
{
static double AdjustPaddingRadius(double radius)
{
if (radius <= 0)
return 0;
return Math.Ceiling(radius) + 1;
}
internal static Thickness GetEffectOutputPadding(this IEffect? effect)
{
if (effect == null)
return default;
if (effect is IBlurEffect blur)
return new Thickness(AdjustPaddingRadius(blur.Radius));
if (effect is IDropShadowEffect dropShadowEffect)
{
var radius = AdjustPaddingRadius(dropShadowEffect.BlurRadius);
var rc = new Rect(-radius, -radius,
radius * 2, radius * 2);
rc = rc.Translate(new(dropShadowEffect.OffsetX, dropShadowEffect.OffsetY));
return new Thickness(Math.Max(0, 0 - rc.X),
Math.Max(0, 0 - rc.Y), Math.Max(0, rc.Right), Math.Max(0, rc.Bottom));
}
throw new ArgumentException("Unknown effect type: " + effect.GetType());
}
/// <summary>
/// Converts a effect to an immutable effect.
/// </summary>
/// <param name="effect">The effect.</param>
/// <returns>
/// The result of calling <see cref="IMutableEffect.ToImmutable"/> if the effect is mutable,
/// otherwise <paramref name="effect"/>.
/// </returns>
public static IImmutableEffect ToImmutable(this IEffect effect)
{
_ = effect ?? throw new ArgumentNullException(nameof(effect));
return (effect as IMutableEffect)?.ToImmutable() ?? (IImmutableEffect)effect;
}
internal static bool EffectEquals(this IImmutableEffect? immutable, IEffect? right)
{
if (immutable == null && right == null)
return true;
if (immutable != null && right != null)
return immutable.Equals(right);
return false;
}
}

83
src/Avalonia.Base/Media/Effects/EffectTransition.cs

@ -0,0 +1,83 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
using Avalonia.Media;
// ReSharper disable once CheckNamespace
namespace Avalonia.Animation;
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="IEffect"/> type.
/// </summary>
public class EffectTransition : Transition<IEffect?>
{
private static readonly BlurEffectAnimator s_blurEffectAnimator = new();
private static readonly DropShadowEffectAnimator s_dropShadowEffectAnimator = new();
private static readonly ImmutableBlurEffect s_DefaultBlur = new ImmutableBlurEffect(0);
private static readonly ImmutableDropShadowDirectionEffect s_DefaultDropShadow = new(0, 0, 0, default, 0);
bool TryWithAnimator<TAnimator, TInterface>(
IObservable<double> progress,
TAnimator animator,
IEffect? oldValue, IEffect? newValue, TInterface defaultValue, [MaybeNullWhen(false)] out IObservable<IEffect?> observable)
where TAnimator : EffectAnimatorBase<TInterface> where TInterface : class, IEffect
{
observable = null;
TInterface? oldI = null, newI = null;
if (oldValue is TInterface oi)
{
oldI = oi;
if (newValue is TInterface ni)
newI = ni;
else if (newValue == null)
newI = defaultValue;
else
return false;
}
else if (newValue is TInterface nv)
{
oldI = defaultValue;
newI = nv;
}
else
return false;
observable = new AnimatorTransitionObservable<IEffect?, Animator<IEffect?>>(animator, progress, Easing, oldI, newI);
return true;
}
public override IObservable<IEffect?> DoTransition(IObservable<double> progress, IEffect? oldValue, IEffect? newValue)
{
if ((oldValue != null || newValue != null)
&& (
TryWithAnimator<BlurEffectAnimator, IBlurEffect>(progress, s_blurEffectAnimator,
oldValue, newValue, s_DefaultBlur, out var observable)
|| TryWithAnimator<DropShadowEffectAnimator, IDropShadowEffect>(progress, s_dropShadowEffectAnimator,
oldValue, newValue, s_DefaultDropShadow, out observable)
))
return observable;
return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue);
}
private sealed class IncompatibleTransitionObservable : TransitionObservableBase<IEffect?>
{
private readonly IEffect? _from;
private readonly IEffect? _to;
public IncompatibleTransitionObservable(IObservable<double> progress, Easing easing, IEffect? from, IEffect? to) : base(progress, easing)
{
_from = from;
_to = to;
}
protected override IEffect? ProduceValue(double progress)
{
return progress >= 0.5 ? _to : _from;
}
}
}

29
src/Avalonia.Base/Media/Effects/IBlurEffect.cs

@ -0,0 +1,29 @@
// ReSharper disable once CheckNamespace
using Avalonia.Animation.Animators;
namespace Avalonia.Media;
public interface IBlurEffect : IEffect
{
double Radius { get; }
}
public class ImmutableBlurEffect : IBlurEffect, IImmutableEffect
{
static ImmutableBlurEffect()
{
EffectAnimator.EnsureRegistered();
}
public ImmutableBlurEffect(double radius)
{
Radius = radius;
}
public double Radius { get; }
public bool Equals(IEffect? other) =>
// ReSharper disable once CompareOfFloatsByEqualityOperator
other is IBlurEffect blur && blur.Radius == Radius;
}

84
src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs

@ -0,0 +1,84 @@
// ReSharper disable once CheckNamespace
using System;
using Avalonia.Animation.Animators;
namespace Avalonia.Media;
public interface IDropShadowEffect : IEffect
{
double OffsetX { get; }
double OffsetY { get; }
double BlurRadius { get; }
Color Color { get; }
double Opacity { get; }
}
internal interface IDirectionDropShadowEffect : IDropShadowEffect
{
double Direction { get; }
double ShadowDepth { get; }
}
public class ImmutableDropShadowEffect : IDropShadowEffect, IImmutableEffect
{
static ImmutableDropShadowEffect()
{
EffectAnimator.EnsureRegistered();
}
public ImmutableDropShadowEffect(double offsetX, double offsetY, double blurRadius, Color color, double opacity)
{
OffsetX = offsetX;
OffsetY = offsetY;
BlurRadius = blurRadius;
Color = color;
Opacity = opacity;
}
public double OffsetX { get; }
public double OffsetY { get; }
public double BlurRadius { get; }
public Color Color { get; }
public double Opacity { get; }
public bool Equals(IEffect? other)
{
return other is IDropShadowEffect d
&& d.OffsetX == OffsetX && d.OffsetY == OffsetY
&& d.BlurRadius == BlurRadius
&& d.Color == Color && d.Opacity == Opacity;
}
}
public class ImmutableDropShadowDirectionEffect : IDirectionDropShadowEffect, IImmutableEffect
{
static ImmutableDropShadowDirectionEffect()
{
EffectAnimator.EnsureRegistered();
}
public ImmutableDropShadowDirectionEffect(double direction, double shadowDepth, double blurRadius, Color color, double opacity)
{
Direction = direction;
ShadowDepth = shadowDepth;
BlurRadius = blurRadius;
Color = color;
Opacity = opacity;
}
public double OffsetX => Math.Cos(Direction * Math.PI / 180) * ShadowDepth;
public double OffsetY => Math.Sin(Direction * Math.PI / 180) * ShadowDepth;
public double Direction { get; }
public double ShadowDepth { get; }
public double BlurRadius { get; }
public Color Color { get; }
public double Opacity { get; }
public bool Equals(IEffect? other)
{
return other is IDropShadowEffect d
&& d.OffsetX == OffsetX && d.OffsetY == OffsetY
&& d.BlurRadius == BlurRadius
&& d.Color == Color && d.Opacity == Opacity;
}
}

26
src/Avalonia.Base/Media/Effects/IEffect.cs

@ -0,0 +1,26 @@
// ReSharper disable once CheckNamespace
using System;
using System.ComponentModel;
namespace Avalonia.Media;
[TypeConverter(typeof(EffectConverter))]
public interface IEffect
{
}
public interface IMutableEffect : IEffect, IAffectsRender
{
/// <summary>
/// Creates an immutable clone of the effect.
/// </summary>
/// <returns>The immutable clone.</returns>
internal IImmutableEffect ToImmutable();
}
public interface IImmutableEffect : IEffect, IEquatable<IEffect>
{
}

62
src/Avalonia.Base/Media/FontManager.cs

@ -30,13 +30,15 @@ namespace Avalonia.Media
_fontFallbacks = options?.FontFallbacks;
DefaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName();
var defaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName();
if (string.IsNullOrEmpty(DefaultFontFamilyName))
if (string.IsNullOrEmpty(defaultFontFamilyName))
{
throw new InvalidOperationException("Default font family name can't be null or empty.");
}
DefaultFontFamily = new FontFamily(defaultFontFamilyName);
AddFontCollection(new SystemFontCollection(this));
}
@ -65,9 +67,9 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets the system's default font family's name.
/// Gets the system's default font family.
/// </summary>
public string DefaultFontFamilyName
public FontFamily DefaultFontFamily
{
get;
}
@ -93,6 +95,11 @@ namespace Avalonia.Media
var fontFamily = typeface.FontFamily;
if(typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName)
{
return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
}
if (fontFamily.Key is FontFamilyKey key)
{
var source = key.Source;
@ -119,9 +126,7 @@ namespace Avalonia.Media
}
}
var familyName = fontFamily.FamilyNames.PrimaryFamilyName.ToUpperInvariant();
if (fontCollection != null && fontCollection.TryGetGlyphTypeface(familyName,
if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName,
typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return true;
@ -133,15 +138,21 @@ namespace Avalonia.Media
}
}
foreach (var familyName in fontFamily.FamilyNames)
for (var i = 0; i < fontFamily.FamilyNames.Count; i++)
{
if (SystemFonts.TryGetGlyphTypeface(familyName.ToUpperInvariant(), typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
var familyName = fontFamily.FamilyNames[i];
if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return true;
if (!fontFamily.FamilyNames.HasFallbacks || glyphTypeface.FamilyName != DefaultFontFamily.Name)
{
return true;
}
}
}
return TryGetGlyphTypeface(new Typeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
//Nothing was found so use the default
return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
}
/// <summary>
@ -201,16 +212,37 @@ namespace Avalonia.Media
{
foreach (var fallback in _fontFallbacks)
{
typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
if (fallback.UnicodeRange.IsInRange(codepoint))
{
typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
return true;
}
}
}
}
if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
//Try to match against fallbacks first
if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks)
{
for (int i = 1; i < fontFamily.FamilyNames.Count; i++)
{
var familyName = fontFamily.FamilyNames[i];
foreach (var fontCollection in _fontCollections.Values)
{
return true;
if (fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
{
return true;
};
}
}
}
return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, fontFamily, culture, out typeface);
//Try to find a match with the system font manager
return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface);
}
}
}

221
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@ -3,14 +3,13 @@ using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
public class EmbeddedFontCollection : IFontCollection
public class EmbeddedFontCollection : FontCollectionBase
{
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
private readonly List<FontFamily> _fontFamilies = new List<FontFamily>(1);
private readonly Uri _key;
@ -24,13 +23,13 @@ namespace Avalonia.Media.Fonts
_source = source;
}
public Uri Key => _key;
public override Uri Key => _key;
public FontFamily this[int index] => _fontFamilies[index];
public override FontFamily this[int index] => _fontFamilies[index];
public int Count => _fontFamilies.Count;
public override int Count => _fontFamilies.Count;
public void Initialize(IFontManagerImpl fontManager)
public override void Initialize(IFontManagerImpl fontManager)
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
@ -42,13 +41,11 @@ namespace Avalonia.Media.Fonts
if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface))
{
var familyName = glyphTypeface.FamilyName.ToUpperInvariant();
if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
{
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
if (_glyphTypefaceCache.TryAdd(familyName, glyphTypefaces))
if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces))
{
_fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName));
}
@ -64,31 +61,10 @@ namespace Avalonia.Media.Fonts
}
}
public void Dispose()
{
foreach (var fontFamily in _fontFamilies)
{
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces))
{
foreach (var glyphTypeface in glyphTypefaces.Values)
{
glyphTypeface.Dispose();
}
}
}
GC.SuppressFinalize(this);
}
public IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
familyName = familyName.ToUpperInvariant();
var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
@ -104,11 +80,9 @@ namespace Avalonia.Media.Fonts
{
var fontFamily = _fontFamilies[i];
if (fontFamily.Name.ToUpperInvariant().StartsWith(familyName.ToUpperInvariant()))
if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture)))
{
familyName = fontFamily.Name.ToUpperInvariant();
if (_glyphTypefaceCache.TryGetValue(familyName, out glyphTypefaces) &&
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{
return true;
@ -121,175 +95,6 @@ namespace Avalonia.Media.Fonts
return false;
}
private static bool TryGetNearestMatch(
ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
return true;
}
if (key.Style != FontStyle.Normal)
{
key = key with { Style = FontStyle.Normal };
}
if (key.Stretch != FontStretch.Normal)
{
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
if (key.Weight != FontWeight.Normal)
{
if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface))
{
return true;
}
}
key = key with { Stretch = FontStretch.Normal };
}
if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
//Take the first glyph typeface we can find.
foreach (var typeface in glyphTypefaces.Values)
{
glyphTypeface = typeface;
return true;
}
return false;
}
private static bool TryFindStretchFallback(
ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
var stretch = (int)key.Stretch;
if (stretch < 5)
{
for (var i = 0; stretch + i < 9; i++)
{
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface))
{
return true;
}
}
}
else
{
for (var i = 0; stretch - i > 1; i++)
{
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface))
{
return true;
}
}
}
return false;
}
private static bool TryFindWeightFallback(
ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? typeface)
{
typeface = null;
var weight = (int)key.Weight;
//If the target weight given is between 400 and 500 inclusive
if (weight >= 400 && weight <= 500)
{
//Look for available weights between the target and 500, in ascending order.
for (var i = 0; weight + i <= 500; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights greater than 500, in ascending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
}
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
if (weight < 400)
{
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
}
//If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
if (weight > 500)
{
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
{
return true;
}
}
}
return false;
}
public override IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
}
}

259
src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs

@ -0,0 +1,259 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
public abstract class FontCollectionBase : IFontCollection
{
protected readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> _glyphTypefaceCache = new();
public abstract Uri Key { get; }
public abstract int Count { get; }
public abstract FontFamily this[int index] { get; }
public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch,
string? familyName, CultureInfo? culture, out Typeface match)
{
match = default;
if (string.IsNullOrEmpty(familyName))
{
foreach (var typefaces in _glyphTypefaceCache.Values)
{
if (TryGetNearestMatch(typefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface))
{
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
match = new Typeface(glyphTypeface.FamilyName, style, weight, stretch);
return true;
}
}
}
}
else
{
if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface))
{
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
match = new Typeface(familyName, style, weight, stretch);
return true;
}
}
}
return false;
}
public abstract void Initialize(IFontManagerImpl fontManager);
public abstract IEnumerator<FontFamily> GetEnumerator();
void IDisposable.Dispose()
{
foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
{
foreach (var pair in glyphTypefaces)
{
pair.Value?.Dispose();
}
}
GC.SuppressFinalize(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
internal static bool TryGetNearestMatch(
ConcurrentDictionary<FontCollectionKey,
IGlyphTypeface?> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
if (key.Style != FontStyle.Normal)
{
key = key with { Style = FontStyle.Normal };
}
if (key.Stretch != FontStretch.Normal)
{
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
if (key.Weight != FontWeight.Normal)
{
if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface))
{
return true;
}
}
key = key with { Stretch = FontStretch.Normal };
}
if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
//Take the first glyph typeface we can find.
foreach (var typeface in glyphTypefaces.Values)
{
if(typeface != null)
{
glyphTypeface = typeface;
return true;
}
}
return false;
}
internal static bool TryFindStretchFallback(
ConcurrentDictionary<FontCollectionKey,
IGlyphTypeface?> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
var stretch = (int)key.Stretch;
if (stretch < 5)
{
for (var i = 0; stretch + i < 9; i++)
{
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
}
}
else
{
for (var i = 0; stretch - i > 1; i++)
{
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
}
}
return false;
}
internal static bool TryFindWeightFallback(
ConcurrentDictionary<FontCollectionKey,
IGlyphTypeface?> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
var weight = (int)key.Weight;
//If the target weight given is between 400 and 500 inclusive
if (weight >= 400 && weight <= 500)
{
//Look for available weights between the target and 500, in ascending order.
for (var i = 0; weight + i <= 500; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
}
//If no match is found, look for available weights greater than 500, in ascending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
}
}
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
if (weight < 400)
{
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
}
}
//If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
if (weight > 500)
{
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null)
{
return true;
}
}
}
return false;
}
}
}

17
src/Avalonia.Base/Media/Fonts/IFontCollection.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts
@ -29,5 +30,21 @@ namespace Avalonia.Media.Fonts
/// <returns>Returns <c>true</c> if a glyph typface can be found; otherwise, <c>false</c></returns>
bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
/// <summary>
/// Tries to match a specified character to a <see cref="Typeface"/> that supports specified font properties.
/// </summary>
/// <param name="codepoint">The codepoint to match against.</param>
/// <param name="fontStyle">The font style.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStretch">The font stretch.</param>
/// <param name="familyName">The family name. This is optional and used for fallback lookup.</param>
/// <param name="culture">The culture.</param>
/// <param name="typeface">The matching <see cref="Typeface"/>.</param>
/// <returns>
/// <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface);
}
}

68
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@ -1,5 +1,4 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@ -7,10 +6,8 @@ using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
internal class SystemFontCollection : IFontCollection
internal class SystemFontCollection : FontCollectionBase
{
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
private readonly FontManager _fontManager;
private readonly string[] _familyNames;
@ -20,9 +17,9 @@ namespace Avalonia.Media.Fonts
_familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames();
}
public Uri Key => FontManager.SystemFontsKey;
public override Uri Key => FontManager.SystemFontsKey;
public FontFamily this[int index]
public override FontFamily this[int index]
{
get
{
@ -32,78 +29,41 @@ namespace Avalonia.Media.Fonts
}
}
public int Count => _familyNames.Length;
public override int Count => _familyNames.Length;
public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
if (familyName == FontFamily.DefaultFontFamilyName)
{
familyName = _fontManager.DefaultFontFamilyName;
}
familyName = familyName.ToUpperInvariant();
glyphTypeface = null;
var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
return true;
}
else
{
if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) &&
glyphTypefaces.TryAdd(key, glyphTypeface))
{
return true;
}
}
}
var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName, (key) => new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>());
if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
if (!glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface);
if (glyphTypefaces.TryAdd(key, glyphTypeface) && _glyphTypefaceCache.TryAdd(familyName, glyphTypefaces))
if (!glyphTypefaces.TryAdd(key, glyphTypeface))
{
return true;
return false;
}
}
return false;
return glyphTypeface != null;
}
public void Initialize(IFontManagerImpl fontManager)
public override void Initialize(IFontManagerImpl fontManager)
{
//We initialize the system font collection during construction.
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<FontFamily> GetEnumerator()
public override IEnumerator<FontFamily> GetEnumerator()
{
foreach (var familyName in _familyNames)
{
yield return new FontFamily(familyName);
}
}
void IDisposable.Dispose()
{
foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
{
foreach (var pair in glyphTypefaces)
{
pair.Value.Dispose();
}
}
GC.SuppressFinalize(this);
}
}
}

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

Loading…
Cancel
Save