Browse Source

Merge branch 'master' into refactor/itemcontainergenerator

pull/9677/head
Wiesław Šoltés 4 years ago
committed by GitHub
parent
commit
17e1d3739b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  2. 5
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  3. 113
      native/Avalonia.Native/src/OSX/PlatformSettings.mm
  4. 4
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  5. 20
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  6. 6
      native/Avalonia.Native/src/OSX/WindowImpl.h
  7. 18
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  8. 1
      native/Avalonia.Native/src/OSX/common.h
  9. 10
      native/Avalonia.Native/src/OSX/main.mm
  10. 25
      packages/Avalonia/AvaloniaBuildTasks.targets
  11. 73
      samples/ControlCatalog/MainView.xaml.cs
  12. 6
      samples/ControlCatalog/Models/StateData.cs
  13. 4
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  14. 2
      samples/ControlCatalog/Pages/NotificationsPage.xaml.cs
  15. 2
      src/Android/Avalonia.Android/AndroidPlatform.cs
  16. 19
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  17. 16
      src/Android/Avalonia.Android/AvaloniaView.cs
  18. 14
      src/Android/Avalonia.Android/IAndroidNavigationService.cs
  19. 94
      src/Android/Avalonia.Android/Platform/AndroidPlatformSettings.cs
  20. 28
      src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs
  21. 13
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  22. 9
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  23. 5
      src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs
  24. 4
      src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
  25. 5
      src/Avalonia.Base/Interactivity/EventRoute.cs
  26. 7
      src/Avalonia.Base/Media/FormattedText.cs
  27. 34
      src/Avalonia.Base/Media/Geometry.cs
  28. 95
      src/Avalonia.Base/Media/GlyphRun.cs
  29. 52
      src/Avalonia.Base/Media/TextDecoration.cs
  30. 275
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  31. 115
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs
  32. 14
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  33. 6
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  34. 49
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  35. 24
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  36. 123
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  37. 4
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  38. 174
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  39. 10
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  40. 4
      src/Avalonia.Base/Media/TextFormatting/TextLine.cs
  41. 25
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  42. 21
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  43. 6
      src/Avalonia.Base/Media/TextFormatting/TextShaper.cs
  44. 31
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  45. 14
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  46. 67
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  47. 6
      src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  48. 14
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  49. 14
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  50. 13
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  51. 44
      src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs
  52. 27
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  53. 6
      src/Avalonia.Base/Platform/IGlyphRunImpl.cs
  54. 15
      src/Avalonia.Base/Platform/IPlatformSettings.cs
  55. 6
      src/Avalonia.Base/Platform/ITextShaperImpl.cs
  56. 2
      src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs
  57. 68
      src/Avalonia.Base/Platform/PlatformColorValues.cs
  58. 18
      src/Avalonia.Base/Platform/SystemNavigationManager.cs
  59. 20
      src/Avalonia.Base/Utilities/ArrayBuilder.cs
  60. 9
      src/Avalonia.Base/Utilities/ArraySlice.cs
  61. 149
      src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs
  62. 3
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  63. 28
      src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs
  64. 26
      src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
  65. 21
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs
  66. 1
      src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs
  67. 2
      src/Avalonia.Controls/ComboBox.cs
  68. 2
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  69. 6
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  70. 2
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  71. 3
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  72. 2
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  73. 2
      src/Avalonia.Controls/Primitives/Popup.cs
  74. 82
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  75. 26
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  76. 18
      src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs
  77. 26
      src/Avalonia.Controls/TextBlock.cs
  78. 6
      src/Avalonia.Controls/TextBox.cs
  79. 6
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  80. 72
      src/Avalonia.Controls/TopLevel.cs
  81. 4
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  82. 4
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  83. 77
      src/Avalonia.FreeDesktop/DBusPlatformSettings.cs
  84. 16
      src/Avalonia.FreeDesktop/DBusSettings.cs
  85. 4
      src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs
  86. 5
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  87. 6
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  88. 5
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  89. 3
      src/Avalonia.Native/AvaloniaNativeDragSource.cs
  90. 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  91. 1
      src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs
  92. 77
      src/Avalonia.Native/NativePlatformSettings.cs
  93. 5
      src/Avalonia.Native/WindowImplBase.cs
  94. 18
      src/Avalonia.Native/avn.idl
  95. 2
      src/Avalonia.X11/X11Platform.cs
  96. 2
      src/Avalonia.X11/X11Window.cs
  97. 31
      src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs
  98. 24
      src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs
  99. 10
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  100. 5
      src/Browser/Avalonia.Browser/Interop/DomHelper.cs

4
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -50,6 +50,7 @@
BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; }; BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; };
BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; }; BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; };
ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; }; ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; };
EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@ -103,6 +104,7 @@
BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = "<group>"; }; BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = "<group>"; };
BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = "<group>"; }; BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = "<group>"; };
ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformSettings.mm; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -163,6 +165,7 @@
1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */, 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */,
37A517B22159597E00FBA241 /* Screens.mm */, 37A517B22159597E00FBA241 /* Screens.mm */,
37C09D8721580FE4006A6758 /* SystemDialogs.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */,
EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */,
AB7A61F02147C815003C5833 /* Products */, AB7A61F02147C815003C5833 /* Products */,
AB661C1C2148230E00291242 /* Frameworks */, AB661C1C2148230E00291242 /* Frameworks */,
18391676ECF0E983F4964357 /* WindowBaseImpl.mm */, 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */,
@ -299,6 +302,7 @@
1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */, 1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */,
18391AC16726CBC45856233B /* AvnWindow.mm in Sources */, 18391AC16726CBC45856233B /* AvnWindow.mm in Sources */,
18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */, 18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */,
EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

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

@ -44,7 +44,7 @@
-(bool) isDialog -(bool) isDialog
{ {
return _parent->IsDialog(); return _parent->IsModal();
} }
-(double) getExtendedTitleBarHeight -(double) getExtendedTitleBarHeight
@ -281,6 +281,9 @@
- (void)windowDidBecomeKey:(NSNotification *_Nonnull)notification - (void)windowDidBecomeKey:(NSNotification *_Nonnull)notification
{ {
if (_parent == nullptr)
return;
_parent->BringToFront(); _parent->BringToFront();
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{

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

@ -0,0 +1,113 @@
#include "common.h"
@interface CocoaThemeObserver : NSObject
-(id)initWithCallback:(IAvnActionCallback *)callback;
@end
class PlatformSettings : public ComSingleObject<IAvnPlatformSettings, &IID_IAvnPlatformSettings>
{
CocoaThemeObserver* observer;
public:
FORWARD_IUNKNOWN()
virtual AvnPlatformThemeVariant GetPlatformTheme() override
{
@autoreleasepool
{
if (@available(macOS 10.14, *))
{
if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAqua
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantLight) {
return AvnPlatformThemeVariant::Light;
} else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameDarkAqua
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantDark) {
return AvnPlatformThemeVariant::Dark;
} else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastAqua
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantLight) {
return AvnPlatformThemeVariant::HighContrastLight;
} else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastDarkAqua
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantDark) {
return AvnPlatformThemeVariant::HighContrastDark;
}
}
return AvnPlatformThemeVariant::Light;
}
}
virtual unsigned int GetAccentColor() override
{
@autoreleasepool
{
if (@available(macOS 10.14, *))
{
auto color = [NSColor controlAccentColor];
return to_argb(color);
}
else
{
return 0;
}
}
}
virtual void RegisterColorsChange(IAvnActionCallback *callback) override
{
if (@available(macOS 10.14, *))
{
observer = [[CocoaThemeObserver alloc] initWithCallback: callback];
[[NSApplication sharedApplication] addObserver:observer forKeyPath:@"effectiveAppearance" options:NSKeyValueObservingOptionNew context:nil];
}
}
private:
unsigned int to_argb(NSColor* color)
{
const CGFloat* components = CGColorGetComponents(color.CGColor);
unsigned int alpha = static_cast<unsigned int>(CGColorGetAlpha(color.CGColor) * 0xFF);
unsigned int red = static_cast<unsigned int>(components[0] * 0xFF);
unsigned int green = static_cast<unsigned int>(components[1] * 0xFF);
unsigned int blue = static_cast<unsigned int>(components[2] * 0xFF);
return (alpha << 24) + (red << 16) + (green << 8) + blue;
}
};
@implementation CocoaThemeObserver
{
ComPtr<IAvnActionCallback> _callback;
}
- (id) initWithCallback:(IAvnActionCallback *)callback{
self = [super init];
if (self) {
_callback = callback;
}
return self;
}
/*- (void)didChangeValueForKey:(NSString *)key {
if([key isEqualToString:@"effectiveAppearance"]) {
_callback->Run();
}
else {
[super didChangeValueForKey:key];
}
}*/
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if([keyPath isEqualToString:@"effectiveAppearance"]) {
_callback->Run();
} else {
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
@end
extern IAvnPlatformSettings* CreatePlatformSettings()
{
return new PlatformSettings();
}

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

@ -92,11 +92,13 @@ BEGIN_INTERFACE_MAP()
virtual HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode) override; virtual HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode) override;
virtual HRESULT SetFrameThemeVariant(AvnPlatformThemeVariant variant) override;
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard *clipboard, IAvnDndResultCallback *cb, IAvnClipboard *clipboard, IAvnDndResultCallback *cb,
void *sourceHandle) override; void *sourceHandle) override;
virtual bool IsDialog(); virtual bool IsModal();
id<AvnWindowProtocol> GetWindowProtocol (); id<AvnWindowProtocol> GetWindowProtocol ();

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

@ -498,6 +498,24 @@ HRESULT WindowBaseImpl::SetTransparencyMode(AvnWindowTransparencyMode mode) {
return S_OK; return S_OK;
} }
HRESULT WindowBaseImpl::SetFrameThemeVariant(AvnPlatformThemeVariant variant) {
START_COM_CALL;
NSAppearanceName appearanceName;
if (@available(macOS 10.14, *))
{
appearanceName = variant == AvnPlatformThemeVariant::Dark ? NSAppearanceNameDarkAqua : NSAppearanceNameAqua;
}
else
{
appearanceName = variant == AvnPlatformThemeVariant::Dark ? NSAppearanceNameVibrantDark : NSAppearanceNameAqua;
}
[Window setAppearance: [NSAppearance appearanceNamed: appearanceName]];
return S_OK;
}
HRESULT WindowBaseImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard *clipboard, IAvnDndResultCallback *cb, void *sourceHandle) { HRESULT WindowBaseImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard *clipboard, IAvnDndResultCallback *cb, void *sourceHandle) {
START_COM_CALL; START_COM_CALL;
@ -542,7 +560,7 @@ HRESULT WindowBaseImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, Av
return S_OK; return S_OK;
} }
bool WindowBaseImpl::IsDialog() { bool WindowBaseImpl::IsModal() {
return false; return false;
} }

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

@ -23,7 +23,7 @@ private:
NSRect _preZoomSize; NSRect _preZoomSize;
bool _transitioningWindowState; bool _transitioningWindowState;
bool _isClientAreaExtended; bool _isClientAreaExtended;
bool _isDialog; bool _isModal;
WindowImpl* _parent; WindowImpl* _parent;
std::list<WindowImpl*> _children; std::list<WindowImpl*> _children;
AvnExtendClientAreaChromeHints _extendClientHints; AvnExtendClientAreaChromeHints _extendClientHints;
@ -91,7 +91,9 @@ BEGIN_INTERFACE_MAP()
virtual HRESULT SetWindowState (AvnWindowState state) override; virtual HRESULT SetWindowState (AvnWindowState state) override;
virtual bool IsDialog() override; virtual bool IsModal() override;
bool IsOwned();
virtual void BringToFront () override; virtual void BringToFront () override;

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

@ -63,7 +63,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
START_COM_CALL; START_COM_CALL;
@autoreleasepool { @autoreleasepool {
_isDialog = isDialog || _parent != nullptr; _isModal = isDialog;
WindowBaseImpl::Show(activate, isDialog); WindowBaseImpl::Show(activate, isDialog);
@ -97,7 +97,7 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) {
_parent = cparent; _parent = cparent;
_isDialog = _parent != nullptr; _isModal = _parent != nullptr;
if(_parent != nullptr && Window != nullptr){ if(_parent != nullptr && Window != nullptr){
// If one tries to show a child window with a minimized parent window, then the parent window will be // If one tries to show a child window with a minimized parent window, then the parent window will be
@ -123,7 +123,7 @@ void WindowImpl::BringToFront()
{ {
if ([Window isVisible] && ![Window isMiniaturized]) if ([Window isVisible] && ![Window isMiniaturized])
{ {
if(IsDialog()) if(IsModal())
{ {
Activate(); Activate();
} }
@ -150,7 +150,7 @@ bool WindowImpl::CanBecomeKeyWindow()
{ {
for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) for(auto iterator = _children.begin(); iterator != _children.end(); iterator++)
{ {
if((*iterator)->IsDialog()) if((*iterator)->IsModal())
{ {
return false; return false;
} }
@ -569,8 +569,12 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) {
} }
} }
bool WindowImpl::IsDialog() { bool WindowImpl::IsModal() {
return _isDialog; return _isModal;
}
bool WindowImpl::IsOwned() {
return _parent != nullptr;
} }
NSWindowStyleMask WindowImpl::GetStyle() { NSWindowStyleMask WindowImpl::GetStyle() {
@ -599,7 +603,7 @@ NSWindowStyleMask WindowImpl::GetStyle() {
break; break;
} }
if (!IsDialog()) { if (!IsOwned()) {
s |= NSWindowStyleMaskMiniaturizable; s |= NSWindowStyleMaskMiniaturizable;
} }

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

@ -27,6 +27,7 @@ extern IAvnMenuItem* CreateAppMenuItem();
extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnMenuItem* CreateAppMenuItemSeparator();
extern IAvnApplicationCommands* CreateApplicationCommands(); extern IAvnApplicationCommands* CreateApplicationCommands();
extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
extern IAvnPlatformSettings* CreatePlatformSettings();
extern void SetAppMenu(IAvnMenu *menu); extern void SetAppMenu(IAvnMenu *menu);
extern void SetServicesMenu (IAvnMenu* menu); extern void SetServicesMenu (IAvnMenu* menu);
extern IAvnMenu* GetAppMenu (); extern IAvnMenu* GetAppMenu ();

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

@ -398,6 +398,16 @@ public:
} }
} }
virtual HRESULT CreatePlatformSettings (IAvnPlatformSettings** ppv) override
{
START_COM_CALL;
@autoreleasepool
{
*ppv = ::CreatePlatformSettings();
return S_OK;
}
}
}; };
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative()

25
packages/Avalonia/AvaloniaBuildTasks.targets

@ -48,8 +48,12 @@
</PropertyGroup> </PropertyGroup>
<Target Name="_GenerateAvaloniaResourcesDependencyCache" BeforeTargets="GenerateAvaloniaResources"> <Target Name="_GenerateAvaloniaResourcesDependencyCache" BeforeTargets="GenerateAvaloniaResources">
<PropertyGroup>
<_AvaloniaResourcesInputsCacheFilePath>$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache</_AvaloniaResourcesInputsCacheFilePath>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<CustomAdditionalGenerateAvaloniaResourcesInputs Include="$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache" /> <CustomAdditionalGenerateAvaloniaResourcesInputs Include="$(_AvaloniaResourcesInputsCacheFilePath)" />
</ItemGroup> </ItemGroup>
<Hash ItemsToHash="@(AvaloniaResource);@(AvaloniaXaml);$(MSBuildAllProjects)"> <Hash ItemsToHash="@(AvaloniaResource);@(AvaloniaXaml);$(MSBuildAllProjects)">
@ -57,7 +61,11 @@
</Hash> </Hash>
<MakeDir Directories="$(IntermediateOutputPath)/Avalonia" /> <MakeDir Directories="$(IntermediateOutputPath)/Avalonia" />
<WriteLinesToFile Overwrite="true" File="$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache" Lines="$(AvaloniaResourcesDependencyHash)" WriteOnlyWhenDifferent="True" /> <WriteLinesToFile Overwrite="true" File="$(_AvaloniaResourcesInputsCacheFilePath)" Lines="$(AvaloniaResourcesDependencyHash)" WriteOnlyWhenDifferent="True" />
<ItemGroup>
<FileWrites Include="$(_AvaloniaResourcesInputsCacheFilePath)" />
</ItemGroup>
</Target> </Target>
<Target Name="GenerateAvaloniaResources" <Target Name="GenerateAvaloniaResources"
@ -68,7 +76,7 @@
Condition="('@(AvaloniaResource->Count())' &gt; 0) or ('@(AvaloniaXaml->Count())' &gt; 0)" Condition="('@(AvaloniaResource->Count())' &gt; 0) or ('@(AvaloniaXaml->Count())' &gt; 0)"
> >
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="@(AvaloniaXaml)"/> <AvaloniaResource Include="@(AvaloniaXaml)" />
</ItemGroup> </ItemGroup>
<GenerateAvaloniaResourcesTask <GenerateAvaloniaResourcesTask
Condition="'$(_AvaloniaUseExternalMSBuild)' != 'true'" Condition="'$(_AvaloniaUseExternalMSBuild)' != 'true'"
@ -76,6 +84,9 @@
Root="$(MSBuildProjectDirectory)" Root="$(MSBuildProjectDirectory)"
Resources="@(AvaloniaResource)" Resources="@(AvaloniaResource)"
ReportImportance="$(AvaloniaXamlReportImportance)"/> ReportImportance="$(AvaloniaXamlReportImportance)"/>
<ItemGroup Condition="'$(_AvaloniaUseExternalMSBuild)' != 'true'">
<FileWrites Include="$(AvaloniaResourcesTemporaryFilePath)" />
</ItemGroup>
<Exec <Exec
Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'" Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"
Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:BuildProjectReferences=false"/> Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:BuildProjectReferences=false"/>
@ -103,6 +114,9 @@
File="$(AvaloniaXamlReferencesTemporaryFilePath)" File="$(AvaloniaXamlReferencesTemporaryFilePath)"
Lines="@(ReferencePathWithRefAssemblies)" Lines="@(ReferencePathWithRefAssemblies)"
Overwrite="true" /> Overwrite="true" />
<ItemGroup Condition="'$(_AvaloniaForceInternalMSBuild)' != 'true'">
<FileWrites Include="$(AvaloniaXamlReferencesTemporaryFilePath)" />
</ItemGroup>
<CompileAvaloniaXamlTask <CompileAvaloniaXamlTask
Condition="'$(_AvaloniaUseExternalMSBuild)' != 'true'" Condition="'$(_AvaloniaUseExternalMSBuild)' != 'true'"
AssemblyFile="@(IntermediateAssembly)" AssemblyFile="@(IntermediateAssembly)"
@ -117,8 +131,9 @@
DelaySign="$(DelaySign)" DelaySign="$(DelaySign)"
SkipXamlCompilation="$(_AvaloniaSkipXamlCompilation)" SkipXamlCompilation="$(_AvaloniaSkipXamlCompilation)"
DebuggerLaunch="$(AvaloniaXamlIlDebuggerLaunch)" DebuggerLaunch="$(AvaloniaXamlIlDebuggerLaunch)"
DefaultCompileBindings="$(AvaloniaUseCompiledBindingsByDefault)" DefaultCompileBindings="$(AvaloniaUseCompiledBindingsByDefault)">
/> <Output TaskParameter="WrittenFilePaths" ItemName="FileWrites" />
</CompileAvaloniaXamlTask>
<Exec <Exec
Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'" Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"
Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileAvaloniaXaml /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:BuildProjectReferences=false"/> Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileAvaloniaXaml /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:BuildProjectReferences=false"/>

73
samples/ControlCatalog/MainView.xaml.cs

@ -3,9 +3,11 @@ using System.Collections;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using ControlCatalog.Models; using ControlCatalog.Models;
using ControlCatalog.Pages; using ControlCatalog.Pages;
@ -14,10 +16,14 @@ namespace ControlCatalog
{ {
public class MainView : UserControl public class MainView : UserControl
{ {
private readonly IPlatformSettings _platformSettings;
public MainView() public MainView()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
_platformSettings = AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>();
PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues());
var sideBar = this.Get<TabControl>("Sidebar"); var sideBar = this.Get<TabControl>("Sidebar");
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime) if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
@ -37,6 +43,15 @@ namespace ControlCatalog
if (themes.SelectedItem is CatalogTheme theme) if (themes.SelectedItem is CatalogTheme theme)
{ {
App.SetThemeVariant(theme); App.SetThemeVariant(theme);
((TopLevel?)this.GetVisualRoot())?.PlatformImpl?.SetFrameThemeVariant(theme switch
{
CatalogTheme.FluentLight => PlatformThemeVariant.Light,
CatalogTheme.FluentDark => PlatformThemeVariant.Dark,
CatalogTheme.SimpleLight => PlatformThemeVariant.Light,
CatalogTheme.SimpleDark => PlatformThemeVariant.Dark,
_ => throw new ArgumentOutOfRangeException()
});
} }
}; };
@ -89,6 +104,62 @@ namespace ControlCatalog
var decorations = this.Get<ComboBox>("Decorations"); var decorations = this.Get<ComboBox>("Decorations");
if (VisualRoot is Window window) if (VisualRoot is Window window)
decorations.SelectedIndex = (int)window.SystemDecorations; decorations.SelectedIndex = (int)window.SystemDecorations;
_platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged;
PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues());
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
_platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged;
}
private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e)
{
var themes = this.Get<ComboBox>("Themes");
var currentTheme = (CatalogTheme?)themes.SelectedItem ?? CatalogTheme.FluentLight;
var newTheme = (currentTheme, e.ThemeVariant) switch
{
(CatalogTheme.FluentDark, PlatformThemeVariant.Light) => CatalogTheme.FluentLight,
(CatalogTheme.FluentLight, PlatformThemeVariant.Dark) => CatalogTheme.FluentDark,
(CatalogTheme.SimpleDark, PlatformThemeVariant.Light) => CatalogTheme.SimpleLight,
(CatalogTheme.SimpleLight, PlatformThemeVariant.Dark) => CatalogTheme.SimpleDark,
_ => currentTheme
};
themes.SelectedItem = newTheme;
Application.Current!.Resources["SystemAccentColor"] = e.AccentColor1;
Application.Current.Resources["SystemAccentColorDark1"] = ChangeColorLuminosity(e.AccentColor1, -0.3);
Application.Current.Resources["SystemAccentColorDark2"] = ChangeColorLuminosity(e.AccentColor1, -0.5);
Application.Current.Resources["SystemAccentColorDark3"] = ChangeColorLuminosity(e.AccentColor1, -0.7);
Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, -0.3);
Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, -0.5);
Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, -0.7);
static Color ChangeColorLuminosity(Color color, double luminosityFactor)
{
var red = (double)color.R;
var green = (double)color.G;
var blue = (double)color.B;
if (luminosityFactor < 0)
{
luminosityFactor = 1 + luminosityFactor;
red *= luminosityFactor;
green *= luminosityFactor;
blue *= luminosityFactor;
}
else if (luminosityFactor >= 0)
{
red = (255 - red) * luminosityFactor + red;
green = (255 - green) * luminosityFactor + green;
blue = (255 - blue) * luminosityFactor + blue;
}
return new Color(color.A, (byte)red, (byte)green, (byte)blue);
}
} }
} }
} }

6
samples/ControlCatalog/Models/StateData.cs

@ -6,10 +6,10 @@ public class StateData
public string Abbreviation { get; private set; } public string Abbreviation { get; private set; }
public string Capital { get; private set; } public string Capital { get; private set; }
public StateData(string name, string abbreviatoin, string capital) public StateData(string name, string abbreviation, string capital)
{ {
Name = name; Name = name;
Abbreviation = abbreviatoin; Abbreviation = abbreviation;
Capital = capital; Capital = capital;
} }
@ -17,4 +17,4 @@ public class StateData
{ {
return Name; return Name;
} }
} }

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

@ -396,8 +396,8 @@ CanPickFolder: {storageProvider.CanPickFolder}";
return item.TryGetUri(out var uri) ? uri.ToString() : item.Name; return item.TryGetUri(out var uri) ? uri.ToString() : item.Name;
} }
Window GetWindow() => this.VisualRoot as Window ?? throw new NullReferenceException("Invalid Owner"); Window GetWindow() => TopLevel.GetTopLevel(this) as Window ?? throw new NullReferenceException("Invalid Owner");
TopLevel GetTopLevel() => this.VisualRoot as TopLevel ?? throw new NullReferenceException("Invalid Owner"); TopLevel GetTopLevel() => TopLevel.GetTopLevel(this) ?? throw new NullReferenceException("Invalid Owner");
private void InitializeComponent() private void InitializeComponent()
{ {

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

@ -27,7 +27,7 @@ namespace ControlCatalog.Pages
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
_viewModel.NotificationManager = new Avalonia.Controls.Notifications.WindowNotificationManager(VisualRoot as TopLevel); _viewModel.NotificationManager = new Avalonia.Controls.Notifications.WindowNotificationManager(TopLevel.GetTopLevel(this));
} }
} }
} }

2
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -43,7 +43,7 @@ namespace Avalonia.Android
.Bind<ICursorFactory>().ToTransient<CursorFactory>() .Bind<ICursorFactory>().ToTransient<CursorFactory>()
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub()) .Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>() .Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>() .Bind<IPlatformSettings>().ToSingleton<AndroidPlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface()) .Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>() .Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer()) .Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())

19
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@ -1,18 +1,14 @@
using System; using System;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Content.Res;
using Android.OS; using Android.OS;
using Android.Runtime; using Android.Runtime;
using Android.Views; using Android.Views;
using AndroidX.AppCompat.App; using AndroidX.AppCompat.App;
using AndroidX.Lifecycle;
using AndroidRect = Android.Graphics.Rect;
namespace Avalonia.Android namespace Avalonia.Android
{ {
public abstract class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler public abstract class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler, IActivityNavigationService
{ {
internal static object ViewContent; internal static object ViewContent;
@ -56,9 +52,18 @@ namespace Avalonia.Android
} }
} }
public override void OnConfigurationChanged(Configuration newConfig) public event EventHandler<AndroidBackRequestedEventArgs> BackRequested;
public override void OnBackPressed()
{ {
base.OnConfigurationChanged(newConfig); var eventArgs = new AndroidBackRequestedEventArgs();
BackRequested?.Invoke(this, eventArgs);
if (!eventArgs.Handled)
{
base.OnBackPressed();
}
} }
protected override void OnDestroy() protected override void OnDestroy()

16
src/Android/Avalonia.Android/AvaloniaView.cs

@ -1,11 +1,14 @@
using System; using System;
using Android.Content; using Android.Content;
using Android.Content.Res;
using Android.Runtime; using Android.Runtime;
using Android.Views; using Android.Views;
using Android.Widget; using Android.Widget;
using Avalonia.Android.Platform;
using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Embedding; using Avalonia.Controls.Embedding;
using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
namespace Avalonia.Android namespace Avalonia.Android
@ -26,6 +29,7 @@ namespace Avalonia.Android
_root.Prepare(); _root.Prepare();
this.SetBackgroundColor(global::Android.Graphics.Color.Transparent); this.SetBackgroundColor(global::Android.Graphics.Color.Transparent);
OnConfigurationChanged();
} }
internal TopLevelImpl TopLevelImpl => _view; internal TopLevelImpl TopLevelImpl => _view;
@ -70,6 +74,18 @@ namespace Avalonia.Android
_timerSubscription?.Dispose(); _timerSubscription?.Dispose();
} }
} }
protected override void OnConfigurationChanged(Configuration newConfig)
{
base.OnConfigurationChanged(newConfig);
OnConfigurationChanged();
}
private void OnConfigurationChanged()
{
var settings = AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>() as AndroidPlatformSettings;
settings?.OnViewConfigurationChanged(Context);
}
class ViewImpl : TopLevelImpl class ViewImpl : TopLevelImpl
{ {

14
src/Android/Avalonia.Android/IAndroidNavigationService.cs

@ -0,0 +1,14 @@
using System;
namespace Avalonia.Android
{
public interface IActivityNavigationService
{
event EventHandler<AndroidBackRequestedEventArgs> BackRequested;
}
public class AndroidBackRequestedEventArgs : EventArgs
{
public bool Handled { get; set; }
}
}

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

@ -0,0 +1,94 @@
using System;
using Android;
using Android.Content;
using Android.Content.Res;
using Android.Graphics;
using Android.Provider;
using Android.Views.Accessibility;
using AndroidX.Core.Content.Resources;
using Avalonia.Media;
using Avalonia.Platform;
using Color = Avalonia.Media.Color;
namespace Avalonia.Android.Platform;
// TODO: ideally should be created per view/activity.
internal class AndroidPlatformSettings : DefaultPlatformSettings
{
private PlatformColorValues _latestValues;
public AndroidPlatformSettings()
{
_latestValues = base.GetColorValues();
}
public override PlatformColorValues GetColorValues()
{
return _latestValues;
}
internal void OnViewConfigurationChanged(Context context)
{
if (context.Resources?.Configuration is null)
{
return;
}
var systemTheme = (context.Resources.Configuration.UiMode & UiMode.NightMask) switch
{
UiMode.NightYes => PlatformThemeVariant.Dark,
UiMode.NightNo => PlatformThemeVariant.Light,
_ => throw new ArgumentOutOfRangeException()
};
if (OperatingSystem.IsAndroidVersionAtLeast(31))
{
// See https://developer.android.com/reference/android/R.color
var accent1 = context.Resources.GetColor(17170494, context.Theme); // Resource.Color.SystemAccent1500
var accent2 = context.Resources.GetColor(17170507, context.Theme); // Resource.Color.SystemAccent2500
var accent3 = context.Resources.GetColor(17170520, context.Theme); // Resource.Color.SystemAccent3500
_latestValues = new PlatformColorValues
{
ThemeVariant = systemTheme,
ContrastPreference = IsHighContrast(context),
AccentColor1 = new Color(accent1.A, accent1.R, accent1.G, accent1.B),
AccentColor2 = new Color(accent2.A, accent2.R, accent2.G, accent2.B),
AccentColor3 = new Color(accent3.A, accent3.R, accent3.G, accent3.B),
};
}
else if (OperatingSystem.IsAndroidVersionAtLeast(23))
{
// See https://developer.android.com/reference/android/R.attr
var array = context.Theme.ObtainStyledAttributes(new[] { 16843829 }); // Resource.Attribute.ColorAccent
var accent = array.GetColor(0, 0);
_latestValues = new PlatformColorValues
{
ThemeVariant = systemTheme,
ContrastPreference = IsHighContrast(context),
AccentColor1 = new Color(accent.A, accent.R, accent.G, accent.B)
};
array.Recycle();
}
else
{
_latestValues = _latestValues with { ThemeVariant = systemTheme };
}
OnColorValuesChanged(_latestValues);
}
private static ColorContrastPreference IsHighContrast(Context context)
{
try
{
return Settings.Secure.GetInt(context.ContentResolver, "high_text_contrast_enabled", 0) == 1
? ColorContrastPreference.High : ColorContrastPreference.NoPreference;
}
catch
{
return ColorContrastPreference.NoPreference;
}
}
}

28
src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs

@ -0,0 +1,28 @@
using System;
using Avalonia.Interactivity;
using Avalonia.Platform;
namespace Avalonia.Android.Platform
{
internal class AndroidSystemNavigationManager : ISystemNavigationManager
{
public event EventHandler<RoutedEventArgs> BackRequested;
public AndroidSystemNavigationManager(IActivityNavigationService? navigationService)
{
if(navigationService != null)
{
navigationService.BackRequested += OnBackRequested;
}
}
private void OnBackRequested(object sender, AndroidBackRequestedEventArgs e)
{
var routedEventArgs = new RoutedEventArgs();
BackRequested?.Invoke(this, routedEventArgs);
e.Handled = routedEventArgs.Handled;
}
}
}

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

@ -26,12 +26,14 @@ using Avalonia.Rendering.Composition;
using Java.Lang; using Java.Lang;
using Math = System.Math; using Math = System.Math;
using AndroidRect = Android.Graphics.Rect; using AndroidRect = Android.Graphics.Rect;
using Window = Android.Views.Window;
using Android.Graphics.Drawables; using Android.Graphics.Drawables;
namespace Avalonia.Android.Platform.SkiaPlatform namespace Avalonia.Android.Platform.SkiaPlatform
{ {
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo,
ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider,
ITopLevelWithSystemNavigationManager
{ {
private readonly IGlPlatformSurface _gl; private readonly IGlPlatformSurface _gl;
private readonly IFramebufferPlatformSurface _framebuffer; private readonly IFramebufferPlatformSurface _framebuffer;
@ -57,6 +59,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
StorageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context); StorageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
SystemNavigationManager = new AndroidSystemNavigationManager(avaloniaView.Context as IActivityNavigationService);
} }
public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
@ -286,6 +290,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public WindowTransparencyLevel TransparencyLevel { get; private set; } public WindowTransparencyLevel TransparencyLevel { get; private set; }
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
// TODO adjust status bar depending on full screen mode.
}
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle; IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle;
@ -300,6 +309,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IStorageProvider StorageProvider { get; } public IStorageProvider StorageProvider { get; }
public ISystemNavigationManager SystemNavigationManager { get; }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{ {
if (TransparencyLevel != transparencyLevel) if (TransparencyLevel != transparencyLevel)

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

@ -189,7 +189,14 @@ namespace Avalonia.Input.GestureRecognizers
var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds);
var distance = speed * elapsedSinceLastTick.TotalSeconds; var distance = speed * elapsedSinceLastTick.TotalSeconds;
_target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance);
_target!.RaiseEvent(scrollGestureEventArgs);
if (!scrollGestureEventArgs.Handled || scrollGestureEventArgs.ShouldEndScrollGesture)
{
EndGesture();
return false;
}
// EndGesture using InertialScrollSpeedEnd only in the direction of scrolling // EndGesture using InertialScrollSpeedEnd only in the direction of scrolling
if (CanVerticallyScroll && CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd && Math.Abs(speed.Y) <= InertialScrollSpeedEnd) if (CanVerticallyScroll && CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)

5
src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs

@ -81,6 +81,10 @@ namespace Avalonia.Input.Platform
{ {
new KeyGesture(Key.Apps) new KeyGesture(Key.Apps)
}; };
Back = new List<KeyGesture>
{
new KeyGesture(Key.Left, KeyModifiers.Alt)
};
} }
public KeyModifiers CommandModifiers { get; set; } public KeyModifiers CommandModifiers { get; set; }
@ -101,5 +105,6 @@ namespace Avalonia.Input.Platform
public List<KeyGesture> MoveCursorToTheStartOfDocumentWithSelection { get; set; } public List<KeyGesture> MoveCursorToTheStartOfDocumentWithSelection { get; set; }
public List<KeyGesture> MoveCursorToTheEndOfDocumentWithSelection { get; set; } public List<KeyGesture> MoveCursorToTheEndOfDocumentWithSelection { get; set; }
public List<KeyGesture> OpenContextMenu { get; set; } public List<KeyGesture> OpenContextMenu { get; set; }
public List<KeyGesture> Back { get; set; }
} }
} }

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

@ -6,6 +6,10 @@ namespace Avalonia.Input
{ {
public int Id { get; } public int Id { get; }
public Vector Delta { get; } public Vector Delta { get; }
/// <summary>
/// When set the ScrollGestureRecognizer should stop its current active scroll gesture.
/// </summary>
public bool ShouldEndScrollGesture { get; set; }
private static int _nextId = 1; private static int _nextId = 1;
public static int GetNextFreeId() => _nextId++; public static int GetNextFreeId() => _nextId++;

5
src/Avalonia.Base/Interactivity/EventRoute.cs

@ -143,10 +143,7 @@ namespace Avalonia.Interactivity
// If we've got to a new control then call any RoutedEvent.Raised listeners. // If we've got to a new control then call any RoutedEvent.Raised listeners.
if (entry.Target != lastTarget) if (entry.Target != lastTarget)
{ {
if (!e.Handled) _event.InvokeRaised(entry.Target, e);
{
_event.InvokeRaised(entry.Target, e);
}
// If this is a direct event and we've already raised events then we're finished. // If this is a direct event and we've already raised events then we're finished.
if (e.Route == RoutingStrategies.Direct && lastTarget is object) if (e.Route == RoutingStrategies.Direct && lastTarget is object)

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

@ -1610,12 +1610,9 @@ namespace Avalonia.Media
var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex); var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex);
var text = _that._text.AsMemory(textSourceCharacterIndex, thatFormatRider.Length);
TextRunProperties properties = (GenericTextRunProperties)thatFormatRider.CurrentElement!; TextRunProperties properties = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
return new TextCharacters(text, properties);
var textCharacters = new TextCharacters(_that._text, textSourceCharacterIndex, thatFormatRider.Length,
properties);
return textCharacters;
} }
} }
} }

34
src/Avalonia.Base/Media/Geometry.cs

@ -1,12 +1,16 @@
using System; using System;
using Avalonia.Platform; using Avalonia.Platform;
using System.ComponentModel;
using System.Globalization;
using Avalonia.Reactive; using Avalonia.Reactive;
namespace Avalonia.Media namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Defines a geometric shape. /// Defines a geometric shape.
/// </summary> /// </summary>
[TypeConverter(typeof(GeometryTypeConverter))]
public abstract class Geometry : AvaloniaObject public abstract class Geometry : AvaloniaObject
{ {
/// <summary> /// <summary>
@ -201,4 +205,32 @@ namespace Avalonia.Media
return new CombinedGeometry(combineMode, geometry1, geometry2, transform); return new CombinedGeometry(combineMode, geometry1, geometry2, transform);
} }
} }
public class GeometryTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is null)
{
throw GetConvertFromException(value);
}
string? source = value as string;
if (source != null)
{
return Geometry.Parse(source);
}
return base.ConvertFrom(context, culture, value);
}
}
} }

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

@ -21,7 +21,7 @@ namespace Avalonia.Media
private Point? _baselineOrigin; private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics; private GlyphRunMetrics? _glyphRunMetrics;
private IReadOnlyList<char> _characters; private ReadOnlyMemory<char> _characters;
private IReadOnlyList<ushort> _glyphIndices; private IReadOnlyList<ushort> _glyphIndices;
private IReadOnlyList<double>? _glyphAdvances; private IReadOnlyList<double>? _glyphAdvances;
private IReadOnlyList<Vector>? _glyphOffsets; private IReadOnlyList<Vector>? _glyphOffsets;
@ -41,7 +41,7 @@ namespace Avalonia.Media
public GlyphRun( public GlyphRun(
IGlyphTypeface glyphTypeface, IGlyphTypeface glyphTypeface,
double fontRenderingEmSize, double fontRenderingEmSize,
IReadOnlyList<char> characters, ReadOnlyMemory<char> characters,
IReadOnlyList<ushort> glyphIndices, IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double>? glyphAdvances = null, IReadOnlyList<double>? glyphAdvances = null,
IReadOnlyList<Vector>? glyphOffsets = null, IReadOnlyList<Vector>? glyphOffsets = null,
@ -141,7 +141,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>. /// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
/// </summary> /// </summary>
public IReadOnlyList<char> Characters public ReadOnlyMemory<char> Characters
{ {
get => _characters; get => _characters;
set => Set(ref _characters, value); set => Set(ref _characters, value);
@ -600,9 +600,9 @@ namespace Avalonia.Media
} }
} }
if (Characters != null) if (!Characters.IsEmpty)
{ {
clusterLength = Characters.Count - characterLength; clusterLength = Characters.Length - characterLength;
} }
else else
{ {
@ -653,10 +653,10 @@ namespace Avalonia.Media
} }
else else
{ {
if (Characters != null && Characters.Count > 0) if (!Characters.IsEmpty)
{ {
firstCluster = 0; firstCluster = 0;
lastCluster = Characters.Count - 1; lastCluster = Characters.Length - 1;
} }
} }
@ -716,14 +716,15 @@ namespace Avalonia.Media
glyphCount = 0; glyphCount = 0;
newLineLength = 0; newLineLength = 0;
var trailingWhitespaceLength = 0; var trailingWhitespaceLength = 0;
var charactersSpan = _characters.Span;
if (Characters != null) if (!charactersSpan.IsEmpty)
{ {
if (GlyphClusters == null) if (GlyphClusters == null)
{ {
for (var i = _characters.Count - 1; i >= 0;) for (var i = charactersSpan.Length - 1; i >= 0;)
{ {
var codepoint = Codepoint.ReadAt(_characters, i, out var count); var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
if (!codepoint.IsWhiteSpace) if (!codepoint.IsWhiteSpace)
{ {
@ -743,55 +744,52 @@ namespace Avalonia.Media
} }
else else
{ {
if (Characters.Count > 0) var characterIndex = charactersSpan.Length - 1;
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
{ {
var characterIndex = Characters.Count - 1; var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
for (var i = GlyphClusters.Count - 1; i >= 0; i--) characterIndex -= characterLength;
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
characterIndex -= characterLength; if (!codepoint.IsWhiteSpace)
{
break;
}
if (!codepoint.IsWhiteSpace) var clusterLength = 1;
{
break;
}
var clusterLength = 1; while (i - 1 >= 0)
{
var nextCluster = GlyphClusters[i - 1];
while (i - 1 >= 0) if (currentCluster == nextCluster)
{ {
var nextCluster = GlyphClusters[i - 1]; clusterLength++;
i--;
if (currentCluster == nextCluster) if(characterIndex >= 0)
{ {
clusterLength++; codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
i--;
if(characterIndex >= 0)
{
codepoint = Codepoint.ReadAt(_characters, characterIndex, out characterLength);
characterIndex -= characterLength; characterIndex -= characterLength;
}
continue;
} }
break; continue;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
} }
trailingWhitespaceLength += clusterLength; break;
}
glyphCount++; if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
} }
trailingWhitespaceLength += clusterLength;
glyphCount++;
} }
} }
} }
@ -804,14 +802,15 @@ namespace Avalonia.Media
glyphCount = 0; glyphCount = 0;
newLineLength = 0; newLineLength = 0;
var trailingWhitespaceLength = 0; var trailingWhitespaceLength = 0;
var charactersSpan = Characters.Span;
if (Characters != null) if (!charactersSpan.IsEmpty)
{ {
if (GlyphClusters == null) if (GlyphClusters == null)
{ {
for (var i = 0; i < Characters.Count;) for (var i = 0; i < charactersSpan.Length;)
{ {
var codepoint = Codepoint.ReadAt(_characters, i, out var count); var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
if (!codepoint.IsWhiteSpace) if (!codepoint.IsWhiteSpace)
{ {
@ -836,7 +835,7 @@ namespace Avalonia.Media
for (var i = 0; i < GlyphClusters.Count; i++) for (var i = 0; i < GlyphClusters.Count; i++)
{ {
var currentCluster = GlyphClusters[i]; var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength); var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
characterIndex += characterLength; characterIndex += characterLength;
@ -918,7 +917,7 @@ namespace Avalonia.Media
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
} }
void IDisposable.Dispose() public void Dispose()
{ {
_glyphRunImpl?.Dispose(); _glyphRunImpl?.Dispose();
} }

52
src/Avalonia.Base/Media/TextDecoration.cs

@ -1,5 +1,10 @@
using Avalonia.Collections; using System;
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Collections.Pooled;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -209,6 +214,51 @@ namespace Avalonia.Media
var pen = new Pen(Stroke ?? defaultBrush, thickness, var pen = new Pen(Stroke ?? defaultBrush, thickness,
new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap);
if (Location != TextDecorationLocation.Strikethrough)
{
var offsetY = glyphRun.BaselineOrigin.Y - origin.Y;
var intersections = glyphRun.GlyphRunImpl.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY));
if (intersections != null && intersections.Count > 0)
{
var last = baselineOrigin.X;
var finalPos = last + glyphRun.Size.Width;
var end = last;
var points = new List<double>();
//math is taken from chrome's source code.
for (var i = 0; i < intersections.Count; i += 2)
{
var start = intersections[i] - thickness;
end = intersections[i + 1] + thickness;
if (start > last && last + textMetrics.FontRenderingEmSize / 12 < start)
{
points.Add(last);
points.Add(start);
}
last = end;
}
if (end < finalPos)
{
points.Add(end);
points.Add(finalPos);
}
for (var i = 0; i < points.Count; i += 2)
{
var a = new Point(points[i], origin.Y);
var b = new Point(points[i + 1], origin.Y);
drawingContext.DrawLine(pen, a, b);
}
return;
}
}
drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Metrics.Width, 0)); drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Metrics.Width, 0));
} }
} }

275
src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs

@ -1,275 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
public readonly struct CharacterBufferRange : IReadOnlyList<char>
{
/// <summary>
/// Getting an empty character string
/// </summary>
public static CharacterBufferRange Empty => new CharacterBufferRange();
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from character array
/// </summary>
/// <param name="characterArray">character array</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
/// <param name="characterLength">character length</param>
public CharacterBufferRange(
char[] characterArray,
int offsetToFirstChar,
int characterLength
)
: this(
new CharacterBufferReference(characterArray, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from string
/// </summary>
/// <param name="characterString">character string</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
/// <param name="characterLength">character length</param>
public CharacterBufferRange(
string characterString,
int offsetToFirstChar,
int characterLength
)
: this(
new CharacterBufferReference(characterString, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from <see cref="CharacterBufferReference"/>
/// </summary>
/// <param name="characterBufferReference">character buffer reference</param>
/// <param name="characterLength">number of characters</param>
public CharacterBufferRange(
CharacterBufferReference characterBufferReference,
int characterLength
)
{
if (characterLength < 0)
{
throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative");
}
int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ?
characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar :
0;
if (characterLength > maxLength)
{
throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}");
}
CharacterBufferReference = characterBufferReference;
Length = characterLength;
}
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from part of another <see cref="CharacterBufferRange"/>
/// </summary>
internal CharacterBufferRange(
CharacterBufferRange characterBufferRange,
int offsetToFirstChar,
int characterLength
) :
this(
characterBufferRange.CharacterBuffer,
characterBufferRange.OffsetToFirstChar + offsetToFirstChar,
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from string
/// </summary>
internal CharacterBufferRange(
string charString
) :
this(
charString,
0,
charString.Length
)
{ }
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from memory buffer
/// </summary>
internal CharacterBufferRange(
ReadOnlyMemory<char> charBuffer,
int offsetToFirstChar,
int characterLength
) :
this(
new CharacterBufferReference(charBuffer, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> by extracting text info from a text run
/// </summary>
internal CharacterBufferRange(TextRun textRun)
{
CharacterBufferReference = textRun.CharacterBufferReference;
Length = textRun.Length;
}
public char this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
#if DEBUG
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
#endif
return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index];
}
}
/// <summary>
/// Gets a reference to the character buffer
/// </summary>
public CharacterBufferReference CharacterBufferReference { get; }
/// <summary>
/// Gets the number of characters in text source character store
/// </summary>
public int Length { get; }
/// <summary>
/// Gets a span from the character buffer range
/// </summary>
public ReadOnlySpan<char> Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length);
/// <summary>
/// Gets the character memory buffer
/// </summary>
internal ReadOnlyMemory<char> CharacterBuffer => CharacterBufferReference.CharacterBuffer;
/// <summary>
/// Gets the character offset relative to the beginning of buffer to
/// the first character of the run
/// </summary>
internal int OffsetToFirstChar => CharacterBufferReference.OffsetToFirstChar;
/// <summary>
/// Indicate whether the character buffer range is empty
/// </summary>
internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0;
internal CharacterBufferRange Take(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new CharacterBufferRange(CharacterBufferReference, length);
}
internal CharacterBufferRange Skip(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
if (length == Length)
{
return new CharacterBufferRange(new CharacterBufferReference(), 0);
}
var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length);
return new CharacterBufferRange(characterBufferReference, Length - length);
}
/// <summary>
/// Compute hash code
/// </summary>
public override int GetHashCode()
{
return CharacterBufferReference.GetHashCode() ^ Length;
}
/// <summary>
/// Test equality with the input object
/// </summary>
/// <param name="obj"> The object to test </param>
public override bool Equals(object? obj)
{
if (obj is CharacterBufferRange range)
{
return Equals(range);
}
return false;
}
/// <summary>
/// Test equality with the input CharacterBufferRange
/// </summary>
/// <param name="value"> The CharacterBufferRange value to test </param>
public bool Equals(CharacterBufferRange value)
{
return CharacterBufferReference.Equals(value.CharacterBufferReference)
&& Length == value.Length;
}
/// <summary>
/// Compare two CharacterBufferRange for equality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right)
{
return left.Equals(right);
}
/// <summary>
/// Compare two CharacterBufferRange for inequality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right)
{
return !(left == right);
}
int IReadOnlyCollection<char>.Count => Length;
public IEnumerator<char> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<char>(this);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

115
src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs

@ -1,115 +0,0 @@
using System;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Text character buffer reference
/// </summary>
public readonly struct CharacterBufferReference : IEquatable<CharacterBufferReference>
{
/// <summary>
/// Construct character buffer reference from character array
/// </summary>
/// <param name="characterArray">character array</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0)
: this(characterArray.AsMemory(), offsetToFirstChar)
{ }
/// <summary>
/// Construct character buffer reference from string
/// </summary>
/// <param name="characterString">character string</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
public CharacterBufferReference(string characterString, int offsetToFirstChar = 0)
: this(characterString.AsMemory(), offsetToFirstChar)
{ }
/// <summary>
/// Construct character buffer reference from memory buffer
/// </summary>
internal CharacterBufferReference(ReadOnlyMemory<char> characterBuffer, int offsetToFirstChar = 0)
{
if (offsetToFirstChar < 0)
{
throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative");
}
// maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset
// even in the case of an empty or null character buffer
var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1);
if (offsetToFirstChar > maxOffset)
{
throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}");
}
CharacterBuffer = characterBuffer;
OffsetToFirstChar = offsetToFirstChar;
}
/// <summary>
/// Gets the character memory buffer
/// </summary>
public ReadOnlyMemory<char> CharacterBuffer { get; }
/// <summary>
/// Gets the character offset relative to the beginning of buffer to
/// the first character of the run
/// </summary>
public int OffsetToFirstChar { get; }
/// <summary>
/// Compute hash code
/// </summary>
public override int GetHashCode()
{
return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode();
}
/// <summary>
/// Test equality with the input object
/// </summary>
/// <param name="obj"> The object to test. </param>
public override bool Equals(object? obj)
{
if (obj is CharacterBufferReference reference)
{
return Equals(reference);
}
return false;
}
/// <summary>
/// Test equality with the input CharacterBufferReference
/// </summary>
/// <param name="value"> The characterBufferReference value to test </param>
public bool Equals(CharacterBufferReference value)
{
return CharacterBuffer.Equals(value.CharacterBuffer);
}
/// <summary>
/// Compare two CharacterBufferReference for equality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right)
{
return left.Equals(right);
}
/// <summary>
/// Compare two CharacterBufferReference for inequality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right)
{
return !(left == right);
}
}
}

14
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@ -7,14 +7,14 @@ namespace Avalonia.Media.TextFormatting
{ {
internal readonly struct FormattedTextSource : ITextSource internal readonly struct FormattedTextSource : ITextSource
{ {
private readonly CharacterBufferRange _text; private readonly string _text;
private readonly TextRunProperties _defaultProperties; private readonly TextRunProperties _defaultProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier; private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier;
public FormattedTextSource(string text, TextRunProperties defaultProperties, public FormattedTextSource(string text, TextRunProperties defaultProperties,
IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier) IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{ {
_text = new CharacterBufferRange(text); _text = text;
_defaultProperties = defaultProperties; _defaultProperties = defaultProperties;
_textModifier = textModifier; _textModifier = textModifier;
} }
@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting
return null; return null;
} }
var runText = _text.Skip(textSourceIndex); var runText = _text.AsSpan(textSourceIndex);
if (runText.IsEmpty) if (runText.IsEmpty)
{ {
@ -35,7 +35,7 @@ namespace Avalonia.Media.TextFormatting
var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier); var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier);
return new TextCharacters(runText.Take(textStyleRun.Length).CharacterBufferReference, textStyleRun.Length, textStyleRun.Value); return new TextCharacters(_text.AsMemory(textSourceIndex, textStyleRun.Length), textStyleRun.Value);
} }
/// <summary> /// <summary>
@ -48,7 +48,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns> /// <returns>
/// The created text style run. /// The created text style run.
/// </returns> /// </returns>
private static ValueSpan<TextRunProperties> CreateTextStyleRun(CharacterBufferRange text, int firstTextSourceIndex, private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySpan<char> text, int firstTextSourceIndex,
TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier) TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{ {
if (textModifier == null || textModifier.Count == 0) if (textModifier == null || textModifier.Count == 0)
@ -122,7 +122,7 @@ namespace Avalonia.Media.TextFormatting
return new ValueSpan<TextRunProperties>(firstTextSourceIndex, length, currentProperties); return new ValueSpan<TextRunProperties>(firstTextSourceIndex, length, currentProperties);
} }
private static int CoerceLength(CharacterBufferRange text, int length) private static int CoerceLength(ReadOnlySpan<char> text, int length)
{ {
var finalLength = 0; var finalLength = 0;
@ -132,7 +132,7 @@ namespace Avalonia.Media.TextFormatting
{ {
var grapheme = graphemeEnumerator.Current; var grapheme = graphemeEnumerator.Current;
finalLength += grapheme.Length; finalLength += grapheme.Text.Length;
if (finalLength >= length) if (finalLength >= length)
{ {

6
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@ -50,14 +50,14 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in lineImpl.TextRuns) foreach (var textRun in lineImpl.TextRuns)
{ {
var text = new CharacterBufferRange(textRun); var text = textRun.Text;
if (text.IsEmpty) if (text.IsEmpty)
{ {
continue; continue;
} }
var lineBreakEnumerator = new LineBreakEnumerator(text); var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
while (lineBreakEnumerator.MoveNext()) while (lineBreakEnumerator.MoveNext())
{ {
@ -84,7 +84,7 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in lineImpl.TextRuns) foreach (var textRun in lineImpl.TextRuns)
{ {
var text = textRun.CharacterBufferReference.CharacterBuffer; var text = textRun.Text;
if (text.IsEmpty) if (text.IsEmpty)
{ {

49
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@ -1,31 +1,39 @@
using System; using System;
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
public sealed class ShapedBuffer : IList<GlyphInfo> public sealed class ShapedBuffer : IList<GlyphInfo>, IDisposable
{ {
private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters(); private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters();
private bool _bufferRented;
public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
this(characterBufferRange, new GlyphInfo[bufferLength], glyphTypeface, fontRenderingEmSize, bidiLevel) public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
this(text,
new ArraySlice<GlyphInfo>(ArrayPool<GlyphInfo>.Shared.Rent(bufferLength), 0, bufferLength),
glyphTypeface,
fontRenderingEmSize,
bidiLevel)
{ {
_bufferRented = true;
Length = bufferLength;
} }
internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) internal ShapedBuffer(ReadOnlyMemory<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
{ {
CharacterBufferRange = characterBufferRange; Text = text;
GlyphInfos = glyphInfos; GlyphInfos = glyphInfos;
GlyphTypeface = glyphTypeface; GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize; FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel; BidiLevel = bidiLevel;
Length = GlyphInfos.Length;
} }
internal ArraySlice<GlyphInfo> GlyphInfos { get; } internal ArraySlice<GlyphInfo> GlyphInfos { get; }
public int Length => GlyphInfos.Length; public int Length { get; }
public IGlyphTypeface GlyphTypeface { get; } public IGlyphTypeface GlyphTypeface { get; }
@ -43,7 +51,7 @@ namespace Avalonia.Media.TextFormatting
public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos); public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
public CharacterBufferRange CharacterBufferRange { get; } public ReadOnlyMemory<char> Text { get; }
/// <summary> /// <summary>
/// Finds a glyph index for given character index. /// Finds a glyph index for given character index.
@ -105,7 +113,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The split result.</returns> /// <returns>The split result.</returns>
internal SplitResult<ShapedBuffer> Split(int length) internal SplitResult<ShapedBuffer> Split(int length)
{ {
if (CharacterBufferRange.Length == length) if (Text.Length == length)
{ {
return new SplitResult<ShapedBuffer>(this, null); return new SplitResult<ShapedBuffer>(this, null);
} }
@ -117,10 +125,10 @@ namespace Avalonia.Media.TextFormatting
var glyphCount = FindGlyphIndex(start + length); var glyphCount = FindGlyphIndex(start + length);
var first = new ShapedBuffer(CharacterBufferRange.Take(length), var first = new ShapedBuffer(Text.Slice(0, length),
GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var second = new ShapedBuffer(CharacterBufferRange.Skip(length), var second = new ShapedBuffer(Text.Slice(length),
GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
return new SplitResult<ShapedBuffer>(first, second); return new SplitResult<ShapedBuffer>(first, second);
@ -260,6 +268,23 @@ namespace Avalonia.Media.TextFormatting
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
} }
public void Dispose()
{
GC.SuppressFinalize(this);
if (_bufferRented)
{
GlyphInfos.ReturnRent();
}
}
~ShapedBuffer()
{
if (_bufferRented)
{
GlyphInfos.ReturnRent();
}
}
} }
public readonly record struct GlyphInfo public readonly record struct GlyphInfo

24
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -6,15 +6,13 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// A text run that holds shaped characters. /// A text run that holds shaped characters.
/// </summary> /// </summary>
public sealed class ShapedTextRun : DrawableTextRun public sealed class ShapedTextRun : DrawableTextRun, IDisposable
{ {
private GlyphRun? _glyphRun; private GlyphRun? _glyphRun;
public ShapedTextRun(ShapedBuffer shapedBuffer, TextRunProperties properties) public ShapedTextRun(ShapedBuffer shapedBuffer, TextRunProperties properties)
{ {
ShapedBuffer = shapedBuffer; ShapedBuffer = shapedBuffer;
CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference;
Length = shapedBuffer.CharacterBufferRange.Length;
Properties = properties; Properties = properties;
TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize); TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize);
} }
@ -26,13 +24,15 @@ namespace Avalonia.Media.TextFormatting
public ShapedBuffer ShapedBuffer { get; } public ShapedBuffer ShapedBuffer { get; }
/// <inheritdoc/> /// <inheritdoc/>
public override CharacterBufferReference CharacterBufferReference { get; } public override ReadOnlyMemory<char> Text
=> ShapedBuffer.Text;
/// <inheritdoc/> /// <inheritdoc/>
public override TextRunProperties Properties { get; } public override TextRunProperties Properties { get; }
/// <inheritdoc/> /// <inheritdoc/>
public override int Length { get; } public override int Length
=> ShapedBuffer.Text.Length;
public TextMetrics TextMetrics { get; } public TextMetrics TextMetrics { get; }
@ -113,6 +113,7 @@ namespace Avalonia.Media.TextFormatting
{ {
length = 0; length = 0;
var currentWidth = 0.0; var currentWidth = 0.0;
var charactersSpan = GlyphRun.Characters.Span;
for (var i = 0; i < ShapedBuffer.Length; i++) for (var i = 0; i < ShapedBuffer.Length; i++)
{ {
@ -123,7 +124,7 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
Codepoint.ReadAt(GlyphRun.Characters, length, out var count); Codepoint.ReadAt(charactersSpan, length, out var count);
length += count; length += count;
currentWidth += advance; currentWidth += advance;
@ -136,6 +137,7 @@ namespace Avalonia.Media.TextFormatting
{ {
length = 0; length = 0;
width = 0; width = 0;
var charactersSpan = GlyphRun.Characters.Span;
for (var i = ShapedBuffer.Length - 1; i >= 0; i--) for (var i = ShapedBuffer.Length - 1; i >= 0; i--)
{ {
@ -146,7 +148,7 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
Codepoint.ReadAt(GlyphRun.Characters, length, out var count); Codepoint.ReadAt(charactersSpan, length, out var count);
length += count; length += count;
width += advance; width += advance;
@ -192,12 +194,18 @@ namespace Avalonia.Media.TextFormatting
return new GlyphRun( return new GlyphRun(
ShapedBuffer.GlyphTypeface, ShapedBuffer.GlyphTypeface,
ShapedBuffer.FontRenderingEmSize, ShapedBuffer.FontRenderingEmSize,
new CharacterBufferRange(CharacterBufferReference, Length), Text,
ShapedBuffer.GlyphIndices, ShapedBuffer.GlyphIndices,
ShapedBuffer.GlyphAdvances, ShapedBuffer.GlyphAdvances,
ShapedBuffer.GlyphOffsets, ShapedBuffer.GlyphOffsets,
ShapedBuffer.GlyphClusters, ShapedBuffer.GlyphClusters,
BidiLevel); BidiLevel);
} }
public void Dispose()
{
_glyphRun?.Dispose();
ShapedBuffer.Dispose();
}
} }
} }

123
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -10,82 +10,34 @@ namespace Avalonia.Media.TextFormatting
public class TextCharacters : TextRun public class TextCharacters : TextRun
{ {
/// <summary> /// <summary>
/// Construct a run of text content from character array /// Constructs a run for text content from a string.
/// </summary> /// </summary>
public TextCharacters( public TextCharacters(string text, TextRunProperties textRunProperties)
char[] characterArray, : this(text.AsMemory(), textRunProperties)
int offsetToFirstChar, {
int length, }
TextRunProperties textRunProperties
) :
this(
new CharacterBufferReference(characterArray, offsetToFirstChar),
length,
textRunProperties
)
{ }
/// <summary>
/// Construct a run for text content from string
/// </summary>
public TextCharacters(
string characterString,
TextRunProperties textRunProperties
) :
this(
characterString,
0, // offsetToFirstChar
(characterString == null) ? 0 : characterString.Length,
textRunProperties
)
{ }
/// <summary>
/// Construct a run for text content from string
/// </summary>
public TextCharacters(
string characterString,
int offsetToFirstChar,
int length,
TextRunProperties textRunProperties
) :
this(
new CharacterBufferReference(characterString, offsetToFirstChar),
length,
textRunProperties
)
{ }
/// <summary> /// <summary>
/// Internal constructor of TextContent /// Constructs a run for text content from a memory region.
/// </summary> /// </summary>
public TextCharacters( public TextCharacters(ReadOnlyMemory<char> text, TextRunProperties textRunProperties)
CharacterBufferReference characterBufferReference,
int length,
TextRunProperties textRunProperties
)
{ {
if (length <= 0)
{
throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero");
}
if (textRunProperties.FontRenderingEmSize <= 0) if (textRunProperties.FontRenderingEmSize <= 0)
{ {
throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero"); throw new ArgumentOutOfRangeException(nameof(textRunProperties), textRunProperties.FontRenderingEmSize,
$"Invalid {nameof(TextRunProperties.FontRenderingEmSize)}");
} }
CharacterBufferReference = characterBufferReference; Text = text;
Length = length;
Properties = textRunProperties; Properties = textRunProperties;
} }
/// <inheritdoc /> /// <inheritdoc />
public override int Length { get; } public override int Length
=> Text.Length;
/// <inheritdoc /> /// <inheritdoc />
public override CharacterBufferReference CharacterBufferReference { get; } public override ReadOnlyMemory<char> Text { get; }
/// <inheritdoc /> /// <inheritdoc />
public override TextRunProperties Properties { get; } public override TextRunProperties Properties { get; }
@ -94,17 +46,19 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of <see cref="UnshapedTextRun"/>. /// Gets a list of <see cref="UnshapedTextRun"/>.
/// </summary> /// </summary>
/// <returns>The shapeable text characters.</returns> /// <returns>The shapeable text characters.</returns>
internal IReadOnlyList<UnshapedTextRun> GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties) internal IReadOnlyList<UnshapedTextRun> GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel,
ref TextRunProperties? previousProperties)
{ {
var shapeableCharacters = new List<UnshapedTextRun>(2); var shapeableCharacters = new List<UnshapedTextRun>(2);
var properties = Properties;
while (characterBufferRange.Length > 0) while (!text.IsEmpty)
{ {
var shapeableRun = CreateShapeableRun(characterBufferRange, Properties, biDiLevel, ref previousProperties); var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties);
shapeableCharacters.Add(shapeableRun); shapeableCharacters.Add(shapeableRun);
characterBufferRange = characterBufferRange.Skip(shapeableRun.Length); text = text.Slice(shapeableRun.Length);
previousProperties = shapeableRun.Properties; previousProperties = shapeableRun.Properties;
} }
@ -115,45 +69,46 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Creates a shapeable text run with unique properties. /// Creates a shapeable text run with unique properties.
/// </summary> /// </summary>
/// <param name="characterBufferRange">The character buffer range to create text runs from.</param> /// <param name="text">The characters to create text runs from.</param>
/// <param name="defaultProperties">The default text run properties.</param> /// <param name="defaultProperties">The default text run properties.</param>
/// <param name="biDiLevel">The bidi level of the run.</param> /// <param name="biDiLevel">The bidi level of the run.</param>
/// <param name="previousProperties"></param> /// <param name="previousProperties"></param>
/// <returns>A list of shapeable text runs.</returns> /// <returns>A list of shapeable text runs.</returns>
private static UnshapedTextRun CreateShapeableRun(CharacterBufferRange characterBufferRange, private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory<char> text,
TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{ {
var defaultTypeface = defaultProperties.Typeface; var defaultTypeface = defaultProperties.Typeface;
var currentTypeface = defaultTypeface; var currentTypeface = defaultTypeface;
var previousTypeface = previousProperties?.Typeface; var previousTypeface = previousProperties?.Typeface;
var textSpan = text.Span;
if (TryGetShapeableLength(characterBufferRange, currentTypeface, null, out var count, out var script)) if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script))
{ {
if (script == Script.Common && previousTypeface is not null) if (script == Script.Common && previousTypeface is not null)
{ {
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _)) if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _))
{ {
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, fallbackCount, return new UnshapedTextRun(text.Slice(0, fallbackCount),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
} }
} }
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface),
biDiLevel); biDiLevel);
} }
if (previousTypeface is not null) if (previousTypeface is not null)
{ {
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _)) if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _))
{ {
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, return new UnshapedTextRun(text.Slice(0, count),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
} }
} }
var codepoint = Codepoint.ReplacementCodepoint; var codepoint = Codepoint.ReplacementCodepoint;
var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count)); var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span);
while (codepointEnumerator.MoveNext()) while (codepointEnumerator.MoveNext())
{ {
@ -173,10 +128,10 @@ namespace Avalonia.Media.TextFormatting
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface); out currentTypeface);
if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _)) if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _))
{ {
//Fallback found //Fallback found
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface),
biDiLevel); biDiLevel);
} }
@ -185,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
var glyphTypeface = currentTypeface.GlyphTypeface; var glyphTypeface = currentTypeface.GlyphTypeface;
var enumerator = new GraphemeEnumerator(characterBufferRange); var enumerator = new GraphemeEnumerator(textSpan);
while (enumerator.MoveNext()) while (enumerator.MoveNext())
{ {
@ -196,23 +151,23 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
count += grapheme.Length; count += grapheme.Text.Length;
} }
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel); return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel);
} }
/// <summary> /// <summary>
/// Tries to get a shapeable length that is supported by the specified typeface. /// Tries to get a shapeable length that is supported by the specified typeface.
/// </summary> /// </summary>
/// <param name="characterBufferRange">The character buffer range to shape.</param> /// <param name="text">The characters to shape.</param>
/// <param name="typeface">The typeface that is used to find matching characters.</param> /// <param name="typeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultTypeface"></param> /// <param name="defaultTypeface"></param>
/// <param name="length">The shapeable length.</param> /// <param name="length">The shapeable length.</param>
/// <param name="script"></param> /// <param name="script"></param>
/// <returns></returns> /// <returns></returns>
internal static bool TryGetShapeableLength( internal static bool TryGetShapeableLength(
CharacterBufferRange characterBufferRange, ReadOnlySpan<char> text,
Typeface typeface, Typeface typeface,
Typeface? defaultTypeface, Typeface? defaultTypeface,
out int length, out int length,
@ -221,7 +176,7 @@ namespace Avalonia.Media.TextFormatting
length = 0; length = 0;
script = Script.Unknown; script = Script.Unknown;
if (characterBufferRange.Length == 0) if (text.IsEmpty)
{ {
return false; return false;
} }
@ -229,7 +184,7 @@ namespace Avalonia.Media.TextFormatting
var font = typeface.GlyphTypeface; var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface?.GlyphTypeface; var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(characterBufferRange); var enumerator = new GraphemeEnumerator(text);
while (enumerator.MoveNext()) while (enumerator.MoveNext())
{ {
@ -264,7 +219,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
length += currentGrapheme.Length; length += currentGrapheme.Text.Length;
} }
return length > 0; return length > 0;

4
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -45,9 +45,7 @@ namespace Avalonia.Media.TextFormatting
{ {
var currentBreakPosition = 0; var currentBreakPosition = 0;
var text = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
var lineBreaker = new LineBreakEnumerator(text);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{ {

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

@ -1,5 +1,7 @@
using System; using System;
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -158,25 +160,21 @@ namespace Avalonia.Media.TextFormatting
{ {
var flowDirection = paragraphProperties.FlowDirection; var flowDirection = paragraphProperties.FlowDirection;
var shapedRuns = new List<TextRun>(); var shapedRuns = new List<TextRun>();
var biDiData = new BidiData((sbyte)flowDirection); using var biDiData = new BidiData((sbyte)flowDirection);
foreach (var textRun in textRuns) foreach (var textRun in textRuns)
{ {
if (textRun.CharacterBufferReference.CharacterBuffer.Length == 0) if (textRun.Text.IsEmpty)
{ {
var characterBuffer = new CharacterBufferReference(new char[textRun.Length]); biDiData.Append(new char[textRun.Length]);
biDiData.Append(new CharacterBufferRange(characterBuffer, textRun.Length));
} }
else else
{ {
var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length); biDiData.Append(textRun.Text.Span);
biDiData.Append(text);
} }
} }
var biDi = new BidiAlgorithm(); using var biDi = new BidiAlgorithm();
biDi.Process(biDiData); biDi.Process(biDiData);
@ -187,10 +185,7 @@ namespace Avalonia.Media.TextFormatting
var processedRuns = new List<TextRun>(textRuns.Count); var processedRuns = new List<TextRun>(textRuns.Count);
foreach (var coalescedRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels)) CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns);
{
processedRuns.AddRange(coalescedRuns);
}
for (var index = 0; index < processedRuns.Count; index++) for (var index = 0; index < processedRuns.Count; index++)
{ {
@ -201,9 +196,7 @@ namespace Avalonia.Media.TextFormatting
case UnshapedTextRun shapeableRun: case UnshapedTextRun shapeableRun:
{ {
var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun }; var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun };
var characterBufferReference = currentRun.CharacterBufferReference; var text = shapeableRun.Text;
var length = currentRun.Length;
var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar;
while (index + 1 < processedRuns.Count) while (index + 1 < processedRuns.Count)
{ {
@ -212,23 +205,14 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
if (shapeableRun.CanShapeTogether(nextRun)) if (shapeableRun.BidiLevel == nextRun.BidiLevel
&& TryJoinContiguousMemories(text, nextRun.Text, out var joinedText)
&& CanShapeTogether(shapeableRun.Properties, nextRun.Properties))
{ {
groupedRuns.Add(nextRun); groupedRuns.Add(nextRun);
length += nextRun.Length;
if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar)
{
offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar;
}
characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter);
index++; index++;
shapeableRun = nextRun; shapeableRun = nextRun;
text = joinedText;
continue; continue;
} }
@ -240,7 +224,7 @@ namespace Avalonia.Media.TextFormatting
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions)); shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
break; break;
} }
@ -256,12 +240,81 @@ namespace Avalonia.Media.TextFormatting
return shapedRuns; return shapedRuns;
} }
/// <summary>
/// Tries to join two potnetially contiguous memory regions.
/// </summary>
/// <param name="x">The first memory region.</param>
/// <param name="y">The second memory region.</param>
/// <param name="joinedMemory">On success, a memory region representing the union of the two regions.</param>
/// <returns>true if the two regions were contigous; false otherwise.</returns>
private static bool TryJoinContiguousMemories(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y,
out ReadOnlyMemory<char> joinedMemory)
{
if (MemoryMarshal.TryGetString(x, out var xString, out var xStart, out var xLength))
{
if (MemoryMarshal.TryGetString(y, out var yString, out var yStart, out var yLength)
&& ReferenceEquals(xString, yString)
&& TryGetContiguousStart(xStart, xLength, yStart, yLength, out var joinedStart))
{
joinedMemory = xString.AsMemory(joinedStart, xLength + yLength);
return true;
}
}
else if (MemoryMarshal.TryGetArray(x, out var xSegment))
{
if (MemoryMarshal.TryGetArray(y, out var ySegment)
&& ReferenceEquals(xSegment.Array, ySegment.Array)
&& TryGetContiguousStart(xSegment.Offset, xSegment.Count, ySegment.Offset, ySegment.Count, out var joinedStart))
{
joinedMemory = xSegment.Array.AsMemory(joinedStart, xSegment.Count + ySegment.Count);
return true;
}
}
else if (MemoryMarshal.TryGetMemoryManager(x, out MemoryManager<char>? xManager, out xStart, out xLength))
{
if (MemoryMarshal.TryGetMemoryManager(y, out MemoryManager<char>? yManager, out var yStart, out var yLength)
&& ReferenceEquals(xManager, yManager)
&& TryGetContiguousStart(xStart, xLength, yStart, yLength, out var joinedStart))
{
joinedMemory = xManager.Memory.Slice(joinedStart, xLength + yLength);
return true;
}
}
joinedMemory = default;
return false;
static bool TryGetContiguousStart(int xStart, int xLength, int yStart, int yLength, out int joinedStart)
{
var xRange = (Start: xStart, Length: xLength);
var yRange = (Start: yStart, Length: yLength);
var (firstRange, secondRange) = xStart <= yStart ? (xRange, yRange) : (yRange, xRange);
if (firstRange.Start + firstRange.Length == secondRange.Start)
{
joinedStart = firstRange.Start;
return true;
}
joinedStart = default;
return false;
}
}
private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y)
=> MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize)
&& x.Typeface == y.Typeface
&& x.BaselineAlignment == y.BaselineAlignment;
private static IReadOnlyList<ShapedTextRun> ShapeTogether( private static IReadOnlyList<ShapedTextRun> ShapeTogether(
IReadOnlyList<UnshapedTextRun> textRuns, CharacterBufferReference text, int length, TextShaperOptions options) IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text, TextShaperOptions options)
{ {
var shapedRuns = new List<ShapedTextRun>(textRuns.Count); var shapedRuns = new List<ShapedTextRun>(textRuns.Count);
var shapedBuffer = TextShaper.Current.ShapeText(text, length, options); var shapedBuffer = TextShaper.Current.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++) for (var i = 0; i < textRuns.Count; i++)
{ {
@ -282,12 +335,14 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
/// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param> /// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param>
/// <param name="levels">The bidi levels.</param> /// <param name="levels">The bidi levels.</param>
/// <param name="processedRuns"></param>
/// <returns></returns> /// <returns></returns>
private static IEnumerable<IReadOnlyList<TextRun>> CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ArraySlice<sbyte> levels) private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ArraySlice<sbyte> levels,
List<TextRun> processedRuns)
{ {
if (levels.Length == 0) if (levels.Length == 0)
{ {
yield break; return;
} }
var levelIndex = 0; var levelIndex = 0;
@ -295,7 +350,7 @@ namespace Avalonia.Media.TextFormatting
TextRunProperties? previousProperties = null; TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null; TextCharacters? currentRun = null;
CharacterBufferRange runText = default; ReadOnlyMemory<char> runText = default;
for (var i = 0; i < textCharacters.Count; i++) for (var i = 0; i < textCharacters.Count; i++)
{ {
@ -306,18 +361,19 @@ namespace Avalonia.Media.TextFormatting
{ {
var drawableRun = textCharacters[i]; var drawableRun = textCharacters[i];
yield return new[] { drawableRun }; processedRuns.Add(drawableRun);
levelIndex += drawableRun.Length; levelIndex += drawableRun.Length;
continue; continue;
} }
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); runText = currentRun.Text;
var runTextSpan = runText.Span;
for (; j < runText.Length;) for (; j < runTextSpan.Length;)
{ {
Codepoint.ReadAt(runText, j, out var count); Codepoint.ReadAt(runTextSpan, j, out var count);
if (levelIndex + 1 == levels.Length) if (levelIndex + 1 == levels.Length)
{ {
@ -327,9 +383,9 @@ namespace Avalonia.Media.TextFormatting
levelIndex++; levelIndex++;
j += count; j += count;
if (j == runText.Length) if (j == runTextSpan.Length)
{ {
yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties); processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties));
runLevel = levels[levelIndex]; runLevel = levels[levelIndex];
@ -342,9 +398,10 @@ namespace Avalonia.Media.TextFormatting
} }
// End of this run // End of this run
yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties); processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties));
runText = runText.Skip(j); runText = runText.Slice(j);
runTextSpan = runText.Span;
j = 0; j = 0;
@ -355,10 +412,10 @@ namespace Avalonia.Media.TextFormatting
if (currentRun is null || runText.IsEmpty) if (currentRun is null || runText.IsEmpty)
{ {
yield break; return;
} }
yield return currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties); processedRuns.AddRange(currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties));
} }
/// <summary> /// <summary>
@ -412,7 +469,7 @@ namespace Avalonia.Media.TextFormatting
{ {
if (TryGetLineBreak(textCharacters, out var runLineBreak)) if (TryGetLineBreak(textCharacters, out var runLineBreak))
{ {
var splitResult = new TextCharacters(textCharacters.CharacterBufferReference, runLineBreak.PositionWrap, var splitResult = new TextCharacters(textCharacters.Text.Slice(0, runLineBreak.PositionWrap),
textCharacters.Properties); textCharacters.Properties);
textRuns.Add(splitResult); textRuns.Add(splitResult);
@ -443,14 +500,14 @@ namespace Avalonia.Media.TextFormatting
{ {
lineBreak = default; lineBreak = default;
if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty) var text = textRun.Text;
if (text.IsEmpty)
{ {
return false; return false;
} }
var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length); var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange);
while (lineBreakEnumerator.MoveNext()) while (lineBreakEnumerator.MoveNext())
{ {
@ -542,8 +599,7 @@ namespace Avalonia.Media.TextFormatting
var glyph = glyphTypeface.GetGlyph(s_empty[0]); var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length); var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection); (sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextRun(shapedBuffer, properties) }; var textRuns = new List<DrawableTextRun> { new ShapedTextRun(shapedBuffer, properties) };
@ -590,10 +646,8 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun) switch (currentRun)
{ {
case ShapedTextRun: case ShapedTextRun:
{ {
var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
var lineBreaker = new LineBreakEnumerator(runText);
while (lineBreaker.MoveNext()) while (lineBreaker.MoveNext())
{ {
@ -652,9 +706,7 @@ namespace Avalonia.Media.TextFormatting
currentRun = textRuns[index]; currentRun = textRuns[index];
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
lineBreaker = new LineBreakEnumerator(runText);
} }
} }
else else
@ -772,9 +824,7 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo); var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
var characterBuffer = textRun.CharacterBufferReference; var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions);
return new ShapedTextRun(shapedBuffer, textRun.Properties); return new ShapedTextRun(shapedBuffer, textRun.Properties);
} }

10
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -8,7 +8,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Represents a multi line text layout. /// Represents a multi line text layout.
/// </summary> /// </summary>
public class TextLayout public class TextLayout : IDisposable
{ {
private readonly ITextSource _textSource; private readonly ITextSource _textSource;
private readonly TextParagraphProperties _paragraphProperties; private readonly TextParagraphProperties _paragraphProperties;
@ -566,5 +566,13 @@ namespace Avalonia.Media.TextFormatting
return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties)); return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties));
} }
public void Dispose()
{
foreach (var line in TextLines)
{
line.Dispose();
}
}
} }
} }

4
src/Avalonia.Base/Media/TextFormatting/TextLine.cs

@ -6,7 +6,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Represents a line of text that is used for text rendering. /// Represents a line of text that is used for text rendering.
/// </summary> /// </summary>
public abstract class TextLine public abstract class TextLine : IDisposable
{ {
/// <summary> /// <summary>
/// Gets the text runs that are contained within a line. /// Gets the text runs that are contained within a line.
@ -207,5 +207,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textLength">number of characters of the specified range</param> /// <param name="textLength">number of characters of the specified range</param>
/// <returns>an array of bounding rectangles.</returns> /// <returns>an array of bounding rectangles.</returns>
public abstract IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceCharacterIndex, int textLength); public abstract IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceCharacterIndex, int textLength);
public abstract void Dispose();
} }
} }

25
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
@ -978,6 +979,17 @@ namespace Avalonia.Media.TextFormatting
return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength); return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength);
} }
public override void Dispose()
{
for (int i = 0; i < _textRuns.Count; i++)
{
if (_textRuns[i] is ShapedTextRun shapedTextRun)
{
shapedTextRun.Dispose();
}
}
}
public TextLineImpl FinalizeLine() public TextLineImpl FinalizeLine()
{ {
_textLineMetrics = CreateLineMetrics(); _textLineMetrics = CreateLineMetrics();
@ -1436,6 +1448,13 @@ namespace Avalonia.Media.TextFormatting
var lineHeight = _paragraphProperties.LineHeight; var lineHeight = _paragraphProperties.LineHeight;
var lastRunIndex = _textRuns.Count - 1;
if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine)
{
lastRunIndex--;
}
for (var index = 0; index < _textRuns.Count; index++) for (var index = 0; index < _textRuns.Count; index++)
{ {
switch (_textRuns[index]) switch (_textRuns[index])
@ -1470,7 +1489,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
if (index == _textRuns.Count - 1) if (index == lastRunIndex)
{ {
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
@ -1490,7 +1509,7 @@ namespace Avalonia.Media.TextFormatting
{ {
case FlowDirection.LeftToRight: case FlowDirection.LeftToRight:
{ {
if (index == _textRuns.Count - 1) if (index == lastRunIndex)
{ {
width = widthIncludingWhitespace; width = widthIncludingWhitespace;
trailingWhitespaceLength = 0; trailingWhitespaceLength = 0;
@ -1502,7 +1521,7 @@ namespace Avalonia.Media.TextFormatting
case FlowDirection.RightToLeft: case FlowDirection.RightToLeft:
{ {
if (index == _textRuns.Count - 1) if (index == lastRunIndex)
{ {
width = widthIncludingWhitespace; width = widthIncludingWhitespace;
trailingWhitespaceLength = 0; trailingWhitespaceLength = 0;

21
src/Avalonia.Base/Media/TextFormatting/TextRun.cs

@ -1,4 +1,5 @@
using System.Diagnostics; using System;
using System.Diagnostics;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Gets the text run's text. /// Gets the text run's text.
/// </summary> /// </summary>
public virtual CharacterBufferReference CharacterBufferReference => default; public virtual ReadOnlyMemory<char> Text => default;
/// <summary> /// <summary>
/// A set of properties shared by every characters in the run /// A set of properties shared by every characters in the run
@ -34,21 +35,7 @@ namespace Avalonia.Media.TextFormatting
_textRun = textRun; _textRun = textRun;
} }
public string Text public string Text => _textRun.Text.ToString();
{
get
{
unsafe
{
var characterBuffer = new CharacterBufferRange(_textRun.CharacterBufferReference, _textRun.Length);
fixed (char* charsPtr = characterBuffer.Span)
{
return new string(charsPtr, 0, _textRun.Length);
}
}
}
}
public TextRunProperties? Properties => _textRun.Properties; public TextRunProperties? Properties => _textRun.Properties;
} }

6
src/Avalonia.Base/Media/TextFormatting/TextShaper.cs

@ -40,14 +40,14 @@ namespace Avalonia.Media.TextFormatting
} }
/// <inheritdoc cref="ITextShaperImpl.ShapeText"/> /// <inheritdoc cref="ITextShaperImpl.ShapeText"/>
public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options = default) public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options = default)
{ {
return _platformImpl.ShapeText(text, length, options); return _platformImpl.ShapeText(text, options);
} }
public ShapedBuffer ShapeText(string text, TextShaperOptions options = default) public ShapedBuffer ShapeText(string text, TextShaperOptions options = default)
{ {
return ShapeText(new CharacterBufferReference(text), text.Length, options); return ShapeText(text.AsMemory(), options);
} }
} }
} }

31
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using Avalonia.Collections.Pooled;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
@ -27,7 +28,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// as much as possible. /// as much as possible.
/// </para> /// </para>
/// </remarks> /// </remarks>
internal sealed class BidiAlgorithm internal struct BidiAlgorithm : IDisposable
{ {
/// <summary> /// <summary>
/// The original BiDiClass classes as provided by the caller /// The original BiDiClass classes as provided by the caller
@ -66,7 +67,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// The forward mapping maps the start index to the end index. /// The forward mapping maps the start index to the end index.
/// The reverse mapping maps the end index to the start index. /// The reverse mapping maps the end index to the start index.
/// </remarks> /// </remarks>
private readonly BidiDictionary<int, int> _isolatePairs = new BidiDictionary<int, int>(); private BidiDictionary<int, int>? _isolatePairs;
/// <summary> /// <summary>
/// The working BiDi classes /// The working BiDi classes
@ -118,7 +119,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary> /// <summary>
/// A stack of pending isolate openings used by FindIsolatePairs() /// A stack of pending isolate openings used by FindIsolatePairs()
/// </summary> /// </summary>
private readonly Stack<int> _pendingIsolateOpenings = new Stack<int>(); private Stack<int>? _pendingIsolateOpenings;
/// <summary> /// <summary>
/// The level of the isolating run currently being processed /// The level of the isolating run currently being processed
@ -184,7 +185,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BidiAlgorithm"/> class. /// Initializes a new instance of the <see cref="BidiAlgorithm"/> class.
/// </summary> /// </summary>
internal BidiAlgorithm() public BidiAlgorithm()
{ {
} }
@ -227,7 +228,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
ArraySlice<sbyte>? outLevels) ArraySlice<sbyte>? outLevels)
{ {
// Reset state // Reset state
_isolatePairs.Clear(); _isolatePairs?.Clear();
_workingClassesBuffer.Clear(); _workingClassesBuffer.Clear();
_levelRuns.Clear(); _levelRuns.Clear();
_resolvedLevelsBuffer.Clear(); _resolvedLevelsBuffer.Clear();
@ -323,7 +324,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
// Skip isolate pairs // Skip isolate pairs
// (Because we're working with a slice, we need to adjust the indices // (Because we're working with a slice, we need to adjust the indices
// we're using for the isolatePairs map) // we're using for the isolatePairs map)
if (_isolatePairs.TryGetValue(data.Start + i, out i)) if (_isolatePairs?.TryGetValue(data.Start + i, out i) == true)
{ {
i -= data.Start; i -= data.Start;
} }
@ -358,7 +359,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_hasIsolates = false; _hasIsolates = false;
// BD9... // BD9...
_pendingIsolateOpenings.Clear(); _pendingIsolateOpenings?.Clear();
for (var i = 0; i < _originalClasses.Length; i++) for (var i = 0; i < _originalClasses.Length; i++)
{ {
@ -370,14 +371,16 @@ namespace Avalonia.Media.TextFormatting.Unicode
case BidiClass.RightToLeftIsolate: case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate: case BidiClass.FirstStrongIsolate:
{ {
_pendingIsolateOpenings ??= new Stack<int>();
_pendingIsolateOpenings.Push(i); _pendingIsolateOpenings.Push(i);
_hasIsolates = true; _hasIsolates = true;
break; break;
} }
case BidiClass.PopDirectionalIsolate: case BidiClass.PopDirectionalIsolate:
{ {
if (_pendingIsolateOpenings.Count > 0) if (_pendingIsolateOpenings?.Count > 0)
{ {
_isolatePairs ??= new BidiDictionary<int, int>();
_isolatePairs.Add(_pendingIsolateOpenings.Pop(), i); _isolatePairs.Add(_pendingIsolateOpenings.Pop(), i);
} }
@ -498,7 +501,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
if (resolvedIsolate == BidiClass.FirstStrongIsolate) if (resolvedIsolate == BidiClass.FirstStrongIsolate)
{ {
if (!_isolatePairs.TryGetValue(i, out var endOfIsolate)) if (_isolatePairs == null || !_isolatePairs.TryGetValue(i, out var endOfIsolate))
{ {
endOfIsolate = _originalClasses.Length; endOfIsolate = _originalClasses.Length;
} }
@ -829,7 +832,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1]; var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1];
var lastType = _originalClasses[lastCharacterIndex]; var lastType = _originalClasses[lastCharacterIndex];
if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) && if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) &&
_isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex)) _isolatePairs?.TryGetValue(lastCharacterIndex, out var nextRunIndex) == true)
{ {
// Find the continuing run index // Find the continuing run index
runIndex = FindRunForIndex(nextRunIndex); runIndex = FindRunForIndex(nextRunIndex);
@ -1714,5 +1717,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
public BidiClass Eos { get; } public BidiClass Eos { get; }
} }
public void Dispose()
{
_workingClassesBuffer.Dispose();
_resolvedLevelsBuffer.Dispose();
_x9Map.Dispose();
_isolatedRunMapping.Dispose();
}
} }
} }

14
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@ -11,7 +11,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Represents a unicode string and all associated attributes /// Represents a unicode string and all associated attributes
/// for each character required for the bidirectional Unicode algorithm /// for each character required for the bidirectional Unicode algorithm
/// </summary> /// </summary>
internal class BidiData internal struct BidiData : IDisposable
{ {
private ArrayBuilder<BidiClass> _classes; private ArrayBuilder<BidiClass> _classes;
private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes; private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes;
@ -64,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Appends text to the bidi data. /// Appends text to the bidi data.
/// </summary> /// </summary>
/// <param name="text">The text to process.</param> /// <param name="text">The text to process.</param>
public void Append(CharacterBufferRange text) public void Append(ReadOnlySpan<char> text)
{ {
_classes.Add(text.Length); _classes.Add(text.Length);
_pairedBracketTypes.Add(text.Length); _pairedBracketTypes.Add(text.Length);
@ -181,5 +181,15 @@ namespace Avalonia.Media.TextFormatting.Unicode
return _tempLevelBuffer.Add(length, false); return _tempLevelBuffer.Add(length, false);
} }
public void Dispose()
{
_classes.Dispose();
_pairedBracketTypes.Dispose();
_pairedBracketValues.Dispose();
_savedClasses.Dispose();
_savedPairedBracketTypes.Dispose();
_tempLevelBuffer.Dispose();
}
} }
} }

67
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

@ -166,72 +166,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <param name="index">The index to read at.</param> /// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param> /// <param name="count">The count of character that were read.</param>
/// <returns></returns> /// <returns></returns>
public static Codepoint ReadAt(IReadOnlyList<char> text, int index, out int count) public static Codepoint ReadAt(ReadOnlySpan<char> text, int index, out int count)
{
count = 1;
if (index >= text.Count)
{
return ReplacementCodepoint;
}
var code = text[index];
ushort hi, low;
//# High surrogate
if (0xD800 <= code && code <= 0xDBFF)
{
hi = code;
if (index + 1 == text.Count)
{
return ReplacementCodepoint;
}
low = text[index + 1];
if (0xDC00 <= low && low <= 0xDFFF)
{
count = 2;
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
}
return ReplacementCodepoint;
}
//# Low surrogate
if (0xDC00 <= code && code <= 0xDFFF)
{
if (index == 0)
{
return ReplacementCodepoint;
}
hi = text[index - 1];
low = code;
if (0xD800 <= hi && hi <= 0xDBFF)
{
count = 2;
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
}
return ReplacementCodepoint;
}
return new Codepoint(code);
}
/// <summary>
/// Reads the <see cref="Codepoint"/> at specified position.
/// </summary>
/// <param name="text">The buffer to read from.</param>
/// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param>
/// <returns></returns>
public static Codepoint ReadAt(CharacterBufferRange text, int index, out int count)
{ {
count = 1; count = 1;

6
src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@ -4,9 +4,9 @@ namespace Avalonia.Media.TextFormatting.Unicode
{ {
public ref struct CodepointEnumerator public ref struct CodepointEnumerator
{ {
private CharacterBufferRange _text; private ReadOnlySpan<char> _text;
public CodepointEnumerator(CharacterBufferRange text) public CodepointEnumerator(ReadOnlySpan<char> text)
{ {
_text = text; _text = text;
Current = Codepoint.ReplacementCodepoint; Current = Codepoint.ReplacementCodepoint;
@ -32,7 +32,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
Current = Codepoint.ReadAt(_text, 0, out var count); Current = Codepoint.ReadAt(_text, 0, out var count);
_text = _text.Skip(count); _text = _text.Slice(count);
return true; return true;
} }

14
src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs

@ -7,11 +7,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary> /// </summary>
public readonly ref struct Grapheme public readonly ref struct Grapheme
{ {
public Grapheme(Codepoint firstCodepoint, int offset, int length) public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
{ {
FirstCodepoint = firstCodepoint; FirstCodepoint = firstCodepoint;
Offset = offset; Text = text;
Length = length;
} }
/// <summary> /// <summary>
@ -20,13 +19,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
public Codepoint FirstCodepoint { get; } public Codepoint FirstCodepoint { get; }
/// <summary> /// <summary>
/// The Offset to the FirstCodepoint /// The text of the grapheme cluster
/// </summary> /// </summary>
public int Offset { get; } public ReadOnlySpan<char> Text { get; }
/// <summary>
/// The length of the grapheme cluster
/// </summary>
public int Length { get; }
} }
} }

14
src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs

@ -3,16 +3,16 @@
// //
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Collections.Generic; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
{ {
public ref struct GraphemeEnumerator public ref struct GraphemeEnumerator
{ {
private CharacterBufferRange _text; private ReadOnlySpan<char> _text;
public GraphemeEnumerator(CharacterBufferRange text) public GraphemeEnumerator(ReadOnlySpan<char> text)
{ {
_text = text; _text = text;
Current = default; Current = default;
@ -185,9 +185,9 @@ namespace Avalonia.Media.TextFormatting.Unicode
Return: Return:
Current = new Grapheme(firstCodepoint, _text.OffsetToFirstChar, processor.CurrentCodeUnitOffset); Current = new Grapheme(firstCodepoint, _text.Slice(0, processor.CurrentCodeUnitOffset));
_text = _text.Skip(processor.CurrentCodeUnitOffset); _text = _text.Slice(processor.CurrentCodeUnitOffset);
return true; // rules GB2, GB999 return true; // rules GB2, GB999
} }
@ -195,10 +195,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
[StructLayout(LayoutKind.Auto)] [StructLayout(LayoutKind.Auto)]
private ref struct Processor private ref struct Processor
{ {
private readonly CharacterBufferRange _buffer; private readonly ReadOnlySpan<char> _buffer;
private int _codeUnitLengthOfCurrentScalar; private int _codeUnitLengthOfCurrentScalar;
internal Processor(CharacterBufferRange buffer) internal Processor(ReadOnlySpan<char> buffer)
{ {
_buffer = buffer; _buffer = buffer;
_codeUnitLengthOfCurrentScalar = 0; _codeUnitLengthOfCurrentScalar = 0;

13
src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@ -3,7 +3,6 @@
// Ported from: https://github.com/SixLabors/Fonts/ // Ported from: https://github.com/SixLabors/Fonts/
using System; using System;
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
{ {
@ -13,7 +12,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary> /// </summary>
public ref struct LineBreakEnumerator public ref struct LineBreakEnumerator
{ {
private readonly IReadOnlyList<char> _text; private readonly ReadOnlySpan<char> _text;
private int _position; private int _position;
private int _lastPosition; private int _lastPosition;
private LineBreakClass _currentClass; private LineBreakClass _currentClass;
@ -29,7 +28,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
private int _lb30a; private int _lb30a;
private bool _lb31; private bool _lb31;
public LineBreakEnumerator(IReadOnlyList<char> text) public LineBreakEnumerator(ReadOnlySpan<char> text)
: this() : this()
{ {
_text = text; _text = text;
@ -63,7 +62,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_lb30a = 0; _lb30a = 0;
} }
while (_position < _text.Count) while (_position < _text.Length)
{ {
_lastPosition = _position; _lastPosition = _position;
var lastClass = _nextClass; var lastClass = _nextClass;
@ -93,11 +92,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
} }
} }
if (_position >= _text.Count) if (_position >= _text.Length)
{ {
if (_lastPosition < _text.Count) if (_lastPosition < _text.Length)
{ {
_lastPosition = _text.Count; _lastPosition = _text.Length;
var required = false; var required = false;

44
src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs

@ -1,4 +1,4 @@
using Avalonia.Utilities; using System;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -7,52 +7,20 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
public sealed class UnshapedTextRun : TextRun public sealed class UnshapedTextRun : TextRun
{ {
public UnshapedTextRun(CharacterBufferReference characterBufferReference, int length, public UnshapedTextRun(ReadOnlyMemory<char> text, TextRunProperties properties, sbyte biDiLevel)
TextRunProperties properties, sbyte biDiLevel)
{ {
CharacterBufferReference = characterBufferReference; Text = text;
Length = length;
Properties = properties; Properties = properties;
BidiLevel = biDiLevel; BidiLevel = biDiLevel;
} }
public override int Length { get; } public override int Length
=> Text.Length;
public override CharacterBufferReference CharacterBufferReference { get; } public override ReadOnlyMemory<char> Text { get; }
public override TextRunProperties Properties { get; } public override TextRunProperties Properties { get; }
public sbyte BidiLevel { get; } public sbyte BidiLevel { get; }
public bool CanShapeTogether(UnshapedTextRun unshapedTextRun)
{
if (!CharacterBufferReference.Equals(unshapedTextRun.CharacterBufferReference))
{
return false;
}
if (BidiLevel != unshapedTextRun.BidiLevel)
{
return false;
}
if (!MathUtilities.AreClose(Properties.FontRenderingEmSize,
unshapedTextRun.Properties.FontRenderingEmSize))
{
return false;
}
if (Properties.Typeface != unshapedTextRun.Properties.Typeface)
{
return false;
}
if (Properties.BaselineAlignment != unshapedTextRun.Properties.BaselineAlignment)
{
return false;
}
return true;
}
} }
} }

27
src/Avalonia.Base/Platform/DefaultPlatformSettings.cs

@ -1,15 +1,15 @@
using System; using System;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Media;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
/// <summary> /// <summary>
/// A default implementation of <see cref="IPlatformSettings"/> for platforms which don't have /// A default implementation of <see cref="IPlatformSettings"/> for platforms.
/// an OS-specific implementation.
/// </summary> /// </summary>
public class DefaultPlatformSettings : IPlatformSettings public class DefaultPlatformSettings : IPlatformSettings
{ {
public Size GetTapSize(PointerType type) public virtual Size GetTapSize(PointerType type)
{ {
return type switch return type switch
{ {
@ -17,7 +17,7 @@ namespace Avalonia.Platform
_ => new(4, 4), _ => new(4, 4),
}; };
} }
public Size GetDoubleTapSize(PointerType type) public virtual Size GetDoubleTapSize(PointerType type)
{ {
return type switch return type switch
{ {
@ -25,8 +25,23 @@ namespace Avalonia.Platform
_ => new(4, 4), _ => new(4, 4),
}; };
} }
public TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500); public virtual TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500);
public TimeSpan HoldWaitDuration { get; set; } = TimeSpan.FromMilliseconds(300); public virtual TimeSpan HoldWaitDuration => TimeSpan.FromMilliseconds(300);
public virtual PlatformColorValues GetColorValues()
{
return new PlatformColorValues
{
ThemeVariant = PlatformThemeVariant.Light
};
}
public event EventHandler<PlatformColorValues>? ColorValuesChanged;
protected void OnColorValuesChanged(PlatformColorValues colorValues)
{
ColorValuesChanged?.Invoke(this, colorValues);
}
} }
} }

6
src/Avalonia.Base/Platform/IGlyphRunImpl.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Platform namespace Avalonia.Platform
@ -7,5 +8,8 @@ namespace Avalonia.Platform
/// Actual implementation of a glyph run that stores platform dependent resources. /// Actual implementation of a glyph run that stores platform dependent resources.
/// </summary> /// </summary>
[Unstable] [Unstable]
public interface IGlyphRunImpl : IDisposable { } public interface IGlyphRunImpl : IDisposable
{
IReadOnlyList<float> GetIntersections(float lowerBound, float upperBound);
}
} }

15
src/Avalonia.Base/Platform/IPlatformSettings.cs

@ -28,6 +28,19 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
TimeSpan GetDoubleTapTime(PointerType type); TimeSpan GetDoubleTapTime(PointerType type);
TimeSpan HoldWaitDuration { get; set; } /// <summary>
/// Holding duration between pointer press and when event is fired.
/// </summary>
TimeSpan HoldWaitDuration { get; }
/// <summary>
/// Gets current system color values including dark mode and accent colors.
/// </summary>
PlatformColorValues GetColorValues();
/// <summary>
/// Raises when current system color values are changed. Including changing of a dark mode and accent colors.
/// </summary>
event EventHandler<PlatformColorValues>? ColorValuesChanged;
} }
} }

6
src/Avalonia.Base/Platform/ITextShaperImpl.cs

@ -1,4 +1,5 @@
using Avalonia.Media.TextFormatting; using System;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Platform namespace Avalonia.Platform
@ -13,9 +14,8 @@ namespace Avalonia.Platform
/// Shapes the specified region within the text and returns a shaped buffer. /// Shapes the specified region within the text and returns a shaped buffer.
/// </summary> /// </summary>
/// <param name="text">The text buffer.</param> /// <param name="text">The text buffer.</param>
/// <param name="length">The length of text.</param>
/// <param name="options">Text shaper options to customize the shaping process.</param> /// <param name="options">Text shaper options to customize the shaping process.</param>
/// <returns>A shaped glyph run.</returns> /// <returns>A shaped glyph run.</returns>
ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options); ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options);
} }
} }

2
src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs

@ -32,7 +32,7 @@ internal class AssemblyDescriptor : IAssemblyDescriptor
Resources.Remove(Constants.AvaloniaResourceName); Resources.Remove(Constants.AvaloniaResourceName);
var indexLength = new BinaryReader(resources).ReadInt32(); var indexLength = new BinaryReader(resources).ReadInt32();
var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(new SlicedStream(resources, 4, indexLength));
var baseOffset = indexLength + 4; var baseOffset = indexLength + 4;
AvaloniaResources = index.ToDictionary(r => GetPathRooted(r), r => (IAssetDescriptor) AvaloniaResources = index.ToDictionary(r => GetPathRooted(r), r => (IAssetDescriptor)
new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size));

68
src/Avalonia.Base/Platform/PlatformColorValues.cs

@ -0,0 +1,68 @@
using Avalonia.Media;
namespace Avalonia.Platform;
/// <summary>
/// System theme variant or mode.
/// </summary>
public enum PlatformThemeVariant
{
Light,
Dark
}
/// <summary>
/// System high contrast preference.
/// </summary>
public enum ColorContrastPreference
{
NoPreference,
High
}
/// <summary>
/// Information about current system color values, including information about dark mode and accent colors.
/// </summary>
public record PlatformColorValues
{
private static Color DefaultAccent => new(255, 0, 120, 215);
private Color _accentColor2, _accentColor3;
/// <summary>
/// System theme variant or mode.
/// </summary>
public PlatformThemeVariant ThemeVariant { get; init; }
/// <summary>
/// System high contrast preference.
/// </summary>
public ColorContrastPreference ContrastPreference { get; init; }
/// <summary>
/// Primary system accent color.
/// </summary>
public Color AccentColor1 { get; init; }
/// <summary>
/// Secondary system accent color. On some platforms can return the same value as <see cref="AccentColor1"/>.
/// </summary>
public Color AccentColor2
{
get => _accentColor2 != default ? _accentColor2 : AccentColor1;
init => _accentColor2 = value;
}
/// <summary>
/// Tertiary system accent color. On some platforms can return the same value as <see cref="AccentColor1"/>.
/// </summary>
public Color AccentColor3
{
get => _accentColor3 != default ? _accentColor3 : AccentColor1;
init => _accentColor3 = value;
}
public PlatformColorValues()
{
AccentColor1 = DefaultAccent;
}
}

18
src/Avalonia.Base/Platform/SystemNavigationManager.cs

@ -0,0 +1,18 @@
using System;
using Avalonia.Interactivity;
using Avalonia.Metadata;
namespace Avalonia.Platform
{
[Unstable]
public interface ITopLevelWithSystemNavigationManager
{
ISystemNavigationManager SystemNavigationManager { get; }
}
[Unstable]
public interface ISystemNavigationManager
{
public event EventHandler<RoutedEventArgs>? BackRequested;
}
}

20
src/Avalonia.Base/Utilities/ArrayBuilder.cs

@ -3,6 +3,7 @@
// Ported from: https://github.com/SixLabors/Fonts/ // Ported from: https://github.com/SixLabors/Fonts/
using System; using System;
using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Avalonia.Utilities namespace Avalonia.Utilities
@ -11,7 +12,7 @@ namespace Avalonia.Utilities
/// A helper type for avoiding allocations while building arrays. /// A helper type for avoiding allocations while building arrays.
/// </summary> /// </summary>
/// <typeparam name="T">The type of item contained in the array.</typeparam> /// <typeparam name="T">The type of item contained in the array.</typeparam>
internal struct ArrayBuilder<T> internal struct ArrayBuilder<T> : IDisposable
where T : struct where T : struct
{ {
private const int DefaultCapacity = 4; private const int DefaultCapacity = 4;
@ -135,7 +136,7 @@ namespace Avalonia.Utilities
} }
// Same expansion algorithm as List<T>. // Same expansion algorithm as List<T>.
var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u; var newCapacity = length == 0 ? DefaultCapacity : length * 2;
if (newCapacity > MaxCoreClrArrayLength) if (newCapacity > MaxCoreClrArrayLength)
{ {
@ -144,14 +145,15 @@ namespace Avalonia.Utilities
if (newCapacity < min) if (newCapacity < min)
{ {
newCapacity = (uint)min; newCapacity = min;
} }
var array = new T[newCapacity]; var array = ArrayPool<T>.Shared.Rent(newCapacity);
if (_size > 0) if (_size > 0)
{ {
Array.Copy(_data!, array, _size); Array.Copy(_data!, array, _size);
ArrayPool<T>.Shared.Return(_data!);
} }
_data = array; _data = array;
@ -180,5 +182,13 @@ namespace Avalonia.Utilities
/// <returns>The <see cref="ArraySlice{T}"/>.</returns> /// <returns>The <see cref="ArraySlice{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArraySlice<T> AsSlice(int start, int length) => new ArraySlice<T>(_data!, start, length); public ArraySlice<T> AsSlice(int start, int length) => new ArraySlice<T>(_data!, start, length);
public void Dispose()
{
if (_data != null)
{
ArrayPool<T>.Shared.Return(_data);
}
}
} }
} }

9
src/Avalonia.Base/Utilities/ArraySlice.cs

@ -3,6 +3,7 @@
// Ported from: https://github.com/SixLabors/Fonts/ // Ported from: https://github.com/SixLabors/Fonts/
using System; using System;
using System.Buffers;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -185,5 +186,13 @@ namespace Avalonia.Utilities
/// <inheritdoc/> /// <inheritdoc/>
int IReadOnlyCollection<T>.Count => Length; int IReadOnlyCollection<T>.Count => Length;
public void ReturnRent()
{
if (_data != null)
{
ArrayPool<T>.Shared.Return(_data);
}
}
} }
} }

149
src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs

@ -1,105 +1,116 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.Serialization; using System.Text;
using System.Xml.Linq;
using System.Linq;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Utilities namespace Avalonia.Utilities
{ {
#if !BUILDTASK #if !BUILDTASK
public public
#endif #endif
static class AvaloniaResourcesIndexReaderWriter static class AvaloniaResourcesIndexReaderWriter
{ {
private const int LastKnownVersion = 1; private const int XmlLegacyVersion = 1;
public static List<AvaloniaResourcesIndexEntry> Read(Stream stream) private const int BinaryCurrentVersion = 2;
public static List<AvaloniaResourcesIndexEntry> ReadIndex(Stream stream)
{ {
var ver = new BinaryReader(stream).ReadInt32(); using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
if (ver > LastKnownVersion)
throw new Exception("Resources index format version is not known");
var assetDoc = XDocument.Load(stream);
XNamespace assetNs = assetDoc.Root!.Attribute("xmlns")!.Value;
List<AvaloniaResourcesIndexEntry> entries =
(from entry in assetDoc.Root.Element(assetNs + "Entries")!.Elements(assetNs + "AvaloniaResourcesIndexEntry")
select new AvaloniaResourcesIndexEntry
{
Path = entry.Element(assetNs + "Path")!.Value,
Offset = int.Parse(entry.Element(assetNs + "Offset")!.Value),
Size = int.Parse(entry.Element(assetNs + "Size")!.Value)
}).ToList();
return entries; var version = reader.ReadInt32();
return version switch
{
XmlLegacyVersion => ReadXmlIndex(),
BinaryCurrentVersion => ReadBinaryIndex(reader),
_ => throw new Exception($"Unknown resources index format version {version}")
};
} }
[RequiresUnreferencedCode("AvaloniaResources uses Data Contract Serialization, which might require unreferenced code")] private static List<AvaloniaResourcesIndexEntry> ReadXmlIndex()
public static void Write(Stream stream, List<AvaloniaResourcesIndexEntry> entries) => throw new NotSupportedException("Found legacy resources index format: please recompile your XAML files");
private static List<AvaloniaResourcesIndexEntry> ReadBinaryIndex(BinaryReader reader)
{ {
new BinaryWriter(stream).Write(LastKnownVersion); var entryCount = reader.ReadInt32();
new DataContractSerializer(typeof(AvaloniaResourcesIndex)).WriteObject(stream, var entries = new List<AvaloniaResourcesIndexEntry>(entryCount);
new AvaloniaResourcesIndex()
{ for (var i = 0; i < entryCount; ++i)
Entries = entries {
entries.Add(new AvaloniaResourcesIndexEntry {
Path = reader.ReadString(),
Offset = reader.ReadInt32(),
Size = reader.ReadInt32()
}); });
}
return entries;
} }
[RequiresUnreferencedCode("AvaloniaResources uses Data Contract Serialization, which might require unreferenced code")] public static void WriteIndex(Stream output, List<AvaloniaResourcesIndexEntry> entries)
public static byte[] Create(Dictionary<string, byte[]> data)
{ {
var sources = data.ToList(); using var writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true);
var offsets = new Dictionary<string, int>();
var coffset = 0; WriteIndex(writer, entries);
foreach (var s in sources) }
private static void WriteIndex(BinaryWriter writer, List<AvaloniaResourcesIndexEntry> entries)
{
writer.Write(BinaryCurrentVersion);
writer.Write(entries.Count);
foreach (var entry in entries)
{ {
offsets[s.Key] = coffset; writer.Write(entry.Path ?? string.Empty);
coffset += s.Value.Length; writer.Write(entry.Offset);
writer.Write(entry.Size);
} }
var index = sources.Select(s => new AvaloniaResourcesIndexEntry }
{
Path = s.Key, public static void WriteResources(Stream output, List<(string Path, int Size, Func<Stream> Open)> resources)
Size = s.Value.Length, {
Offset = offsets[s.Key] var entries = new List<AvaloniaResourcesIndexEntry>(resources.Count);
}).ToList(); var offset = 0;
var output = new MemoryStream();
var ms = new MemoryStream(); foreach (var resource in resources)
AvaloniaResourcesIndexReaderWriter.Write(ms, index);
new BinaryWriter(output).Write((int)ms.Length);
ms.Position = 0;
ms.CopyTo(output);
foreach (var s in sources)
{ {
output.Write(s.Value,0,s.Value.Length); entries.Add(new AvaloniaResourcesIndexEntry
{
Path = resource.Path,
Offset = offset,
Size = resource.Size
});
offset += resource.Size;
} }
return output.ToArray(); using var writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true);
} writer.Write(0); // index size placeholder, overwritten below
}
[DataContract] var posBeforeEntries = output.Position;
#if !BUILDTASK WriteIndex(writer, entries);
public
#endif var posAfterEntries = output.Position;
class AvaloniaResourcesIndex var indexSize = (int) (posAfterEntries - posBeforeEntries);
{ output.Position = 0L;
[DataMember] writer.Write(indexSize);
public List<AvaloniaResourcesIndexEntry> Entries { get; set; } = new List<AvaloniaResourcesIndexEntry>(); output.Position = posAfterEntries;
foreach (var resource in resources)
{
using var resourceStream = resource.Open();
resourceStream.CopyTo(output);
}
}
} }
[DataContract]
#if !BUILDTASK #if !BUILDTASK
public public
#endif #endif
class AvaloniaResourcesIndexEntry class AvaloniaResourcesIndexEntry
{ {
[DataMember]
public string? Path { get; set; } public string? Path { get; set; }
[DataMember]
public int Offset { get; set; } public int Offset { get; set; }
[DataMember]
public int Size { get; set; } public int Size { get; set; }
} }
} }

3
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -18,6 +18,9 @@
<Compile Include="../Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs"> <Compile Include="../Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs">
<Link>Shared/AvaloniaResourcesIndex.cs</Link> <Link>Shared/AvaloniaResourcesIndex.cs</Link>
</Compile> </Compile>
<Compile Include="../Avalonia.Base/Platform/Internal/Constants.cs">
<Link>Shared/Constants.cs</Link>
</Compile>
<Compile Include="../Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaResourceXamlInfo.cs"> <Compile Include="../Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaResourceXamlInfo.cs">
<Link>Shared/AvaloniaResourceXamlInfo.cs</Link> <Link>Shared/AvaloniaResourceXamlInfo.cs</Link>
</Compile> </Compile>

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

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Microsoft.Build.Framework; using Microsoft.Build.Framework;
@ -10,9 +11,10 @@ namespace Avalonia.Build.Tasks
public bool Execute() public bool Execute()
{ {
Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance); Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance);
var writtenFilePaths = new List<string>();
OutputPath = OutputPath ?? AssemblyFile; OutputPath ??= AssemblyFile;
RefOutputPath = RefOutputPath ?? RefAssemblyFile; RefOutputPath ??= RefAssemblyFile;
var outputPdb = GetPdbPath(OutputPath); var outputPdb = GetPdbPath(OutputPath);
var input = AssemblyFile; var input = AssemblyFile;
var refInput = RefOutputPath; var refInput = RefOutputPath;
@ -21,8 +23,9 @@ namespace Avalonia.Build.Tasks
if (OriginalCopyPath != null) if (OriginalCopyPath != null)
{ {
var originalCopyPathRef = Path.ChangeExtension(OriginalCopyPath, ".ref.dll"); var originalCopyPathRef = Path.ChangeExtension(OriginalCopyPath, ".ref.dll");
File.Copy(AssemblyFile, OriginalCopyPath, true); File.Copy(AssemblyFile, OriginalCopyPath, true);
writtenFilePaths.Add(OriginalCopyPath);
input = OriginalCopyPath; input = OriginalCopyPath;
File.Delete(AssemblyFile); File.Delete(AssemblyFile);
@ -30,6 +33,7 @@ namespace Avalonia.Build.Tasks
{ {
var copyPdb = GetPdbPath(OriginalCopyPath); var copyPdb = GetPdbPath(OriginalCopyPath);
File.Copy(inputPdb, copyPdb, true); File.Copy(inputPdb, copyPdb, true);
writtenFilePaths.Add(copyPdb);
File.Delete(inputPdb); File.Delete(inputPdb);
inputPdb = copyPdb; inputPdb = copyPdb;
} }
@ -39,6 +43,7 @@ namespace Avalonia.Build.Tasks
// We also copy ref assembly just for case if needed later for testing. // We also copy ref assembly just for case if needed later for testing.
// But do not remove the original one, as MSBuild actually complains about it with multi-thread compiling. // But do not remove the original one, as MSBuild actually complains about it with multi-thread compiling.
File.Copy(RefAssemblyFile, originalCopyPathRef, true); File.Copy(RefAssemblyFile, originalCopyPathRef, true);
writtenFilePaths.Add(originalCopyPathRef);
refInput = originalCopyPathRef; refInput = originalCopyPathRef;
} }
} }
@ -53,13 +58,25 @@ namespace Avalonia.Build.Tasks
ProjectDirectory, VerifyIl, DefaultCompileBindings, outputImportance, ProjectDirectory, VerifyIl, DefaultCompileBindings, outputImportance,
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, SkipXamlCompilation, DebuggerLaunch); (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, SkipXamlCompilation, DebuggerLaunch);
if (!res.Success) if (!res.Success)
{
WrittenFilePaths = writtenFilePaths.ToArray();
return false; return false;
}
if (!res.WrittenFile) if (!res.WrittenFile)
{ {
File.Copy(input, OutputPath, true); File.Copy(input, OutputPath, true);
if(File.Exists(inputPdb)) if (File.Exists(inputPdb))
File.Copy(inputPdb, outputPdb, true); File.Copy(inputPdb, outputPdb, true);
} }
else if (!string.IsNullOrWhiteSpace(RefOutputPath) && File.Exists(RefOutputPath))
writtenFilePaths.Add(RefOutputPath);
writtenFilePaths.Add(OutputPath);
if (File.Exists(outputPdb))
writtenFilePaths.Add(outputPdb);
WrittenFilePaths = writtenFilePaths.ToArray();
return true; return true;
} }
@ -103,5 +120,8 @@ namespace Avalonia.Build.Tasks
public ITaskHost HostObject { get; set; } public ITaskHost HostObject { get; set; }
public bool DebuggerLaunch { get; set; } public bool DebuggerLaunch { get; set; }
[Output]
public string[] WrittenFilePaths { get; private set; } = Array.Empty<string>();
} }
} }

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

@ -77,29 +77,9 @@ namespace Avalonia.Build.Tasks
private void Pack(Stream output, List<Source> sources) private void Pack(Stream output, List<Source> sources)
{ {
var offsets = new Dictionary<Source, int>(); AvaloniaResourcesIndexReaderWriter.WriteResources(
var coffset = 0; output,
foreach (var s in sources) sources.Select(source => (source.Path, source.Size, (Func<Stream>) source.Open)).ToList());
{
offsets[s] = coffset;
coffset += s.Size;
}
var index = sources.Select(s => new AvaloniaResourcesIndexEntry
{
Path = s.Path,
Size = s.Size,
Offset = offsets[s]
}).ToList();
var ms = new MemoryStream();
AvaloniaResourcesIndexReaderWriter.Write(ms, index);
new BinaryWriter(output).Write((int)ms.Length);
ms.Position = 0;
ms.CopyTo(output);
foreach (var s in sources)
{
using (var input = s.Open())
input.CopyTo(output);
}
} }
private bool PreProcessXamlFiles(List<Source> sources) private bool PreProcessXamlFiles(List<Source> sources)

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

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Avalonia.Platform.Internal;
using Avalonia.Utilities; using Avalonia.Utilities;
using Mono.Cecil; using Mono.Cecil;
using Mono.Cecil.Cil; using Mono.Cecil.Cil;
@ -34,13 +36,13 @@ namespace Avalonia.Build.Tasks
{ {
_asm = asm; _asm = asm;
_embedded = ((EmbeddedResource)asm.MainModule.Resources.FirstOrDefault(r => _embedded = ((EmbeddedResource)asm.MainModule.Resources.FirstOrDefault(r =>
r.ResourceType == ResourceType.Embedded && r.Name == "!AvaloniaResources")); r.ResourceType == ResourceType.Embedded && r.Name == Constants.AvaloniaResourceName));
if (_embedded == null) if (_embedded == null)
return; return;
using (var stream = _embedded.GetResourceStream()) using (var stream = _embedded.GetResourceStream())
{ {
var br = new BinaryReader(stream); var br = new BinaryReader(stream);
var index = AvaloniaResourcesIndexReaderWriter.Read(new MemoryStream(br.ReadBytes(br.ReadInt32()))); var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(new MemoryStream(br.ReadBytes(br.ReadInt32())));
var baseOffset = stream.Position; var baseOffset = stream.Position;
foreach (var e in index) foreach (var e in index)
{ {
@ -61,9 +63,18 @@ namespace Avalonia.Build.Tasks
if (_resources.Count == 0) if (_resources.Count == 0)
return; return;
_embedded = new EmbeddedResource("!AvaloniaResources", ManifestResourceAttributes.Public, var output = new MemoryStream();
AvaloniaResourcesIndexReaderWriter.Create(_resources.ToDictionary(x => x.Key,
x => x.Value.FileContents))); AvaloniaResourcesIndexReaderWriter.WriteResources(
output,
_resources.Select(x => (
Path: x.Key,
Size: x.Value.FileContents.Length,
Open: (Func<Stream>) (() => new MemoryStream(x.Value.FileContents))
)).ToList());
output.Position = 0L;
_embedded = new EmbeddedResource(Constants.AvaloniaResourceName, ManifestResourceAttributes.Public, output);
_asm.MainModule.Resources.Add(_embedded); _asm.MainModule.Resources.Add(_embedded);
} }

1
src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs

@ -2,7 +2,6 @@
using Avalonia.Reactive; using Avalonia.Reactive;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Reactive;
namespace Avalonia.Controls.Utils namespace Avalonia.Controls.Utils
{ {

2
src/Avalonia.Controls/ComboBox.cs

@ -361,7 +361,7 @@ namespace Avalonia.Controls
_subscriptionsOnOpen.Clear(); _subscriptionsOnOpen.Clear();
var toplevel = this.GetVisualRoot() as TopLevel; var toplevel = TopLevel.GetTopLevel(this);
if (toplevel != null) if (toplevel != null)
{ {
toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>

2
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -58,6 +58,8 @@ namespace Avalonia.Controls.Embedding.Offscreen
public Action<WindowTransparencyLevel>? TransparencyLevelChanged { get; set; } public Action<WindowTransparencyLevel>? TransparencyLevelChanged { get; set; }
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { }
/// <inheritdoc/> /// <inheritdoc/>
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);

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

@ -163,6 +163,12 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
WindowTransparencyLevel TransparencyLevel { get; } WindowTransparencyLevel TransparencyLevel { get; }
/// <summary>
/// Sets the <see cref="PlatformThemeVariant"/> on the frame if it should be dark or light.
/// Also applies for the mobile status bar.
/// </summary>
void SetFrameThemeVariant(PlatformThemeVariant themeVariant);
/// <summary> /// <summary>
/// Gets the <see cref="AcrylicPlatformCompensationLevels"/> for the platform. /// Gets the <see cref="AcrylicPlatformCompensationLevels"/> for the platform.
/// </summary> /// </summary>

2
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -428,6 +428,8 @@ namespace Avalonia.Controls.Presenters
Offset = newOffset; Offset = newOffset;
e.Handled = !IsScrollChainingEnabled || offsetChanged; e.Handled = !IsScrollChainingEnabled || offsetChanged;
e.ShouldEndScrollGesture = !IsScrollChainingEnabled && !offsetChanged;
} }
} }

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

@ -588,6 +588,7 @@ namespace Avalonia.Controls.Presenters
protected virtual void InvalidateTextLayout() protected virtual void InvalidateTextLayout()
{ {
_textLayout?.Dispose();
_textLayout = null; _textLayout = null;
InvalidateMeasure(); InvalidateMeasure();
@ -597,6 +598,7 @@ namespace Avalonia.Controls.Presenters
{ {
_constraint = availableSize; _constraint = availableSize;
_textLayout?.Dispose();
_textLayout = null; _textLayout = null;
InvalidateArrange(); InvalidateArrange();
@ -622,6 +624,7 @@ namespace Avalonia.Controls.Presenters
_constraint = new Size(Math.Ceiling(finalSize.Width), double.PositiveInfinity); _constraint = new Size(Math.Ceiling(finalSize.Width), double.PositiveInfinity);
_textLayout?.Dispose();
_textLayout = null; _textLayout = null;
return finalSize; return finalSize;

2
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@ -123,7 +123,7 @@ namespace Avalonia.Controls.Primitives
public static IPopupHost CreatePopupHost(Visual target, IAvaloniaDependencyResolver? dependencyResolver) public static IPopupHost CreatePopupHost(Visual target, IAvaloniaDependencyResolver? dependencyResolver)
{ {
var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup(); var platform = TopLevel.GetTopLevel(target)?.PlatformImpl?.CreatePopup();
if (platform != null) if (platform != null)
return new PopupRoot((TopLevel)target.GetVisualRoot()!, platform, dependencyResolver); return new PopupRoot((TopLevel)target.GetVisualRoot()!, platform, dependencyResolver);

2
src/Avalonia.Controls/Primitives/Popup.cs

@ -358,7 +358,7 @@ namespace Avalonia.Controls.Primitives
return; return;
} }
var topLevel = placementTarget.VisualRoot as TopLevel; var topLevel = TopLevel.GetTopLevel(placementTarget);
if (topLevel == null) if (topLevel == null)
{ {

82
src/Avalonia.Controls/Primitives/ToggleButton.cs

@ -32,26 +32,42 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Defines the <see cref="Checked"/> event. /// Defines the <see cref="Checked"/> event.
/// </summary> /// </summary>
[Obsolete("Use IsCheckedChangedEvent instead.")]
public static readonly RoutedEvent<RoutedEventArgs> CheckedEvent = public static readonly RoutedEvent<RoutedEventArgs> CheckedEvent =
RoutedEvent.Register<ToggleButton, RoutedEventArgs>(nameof(Checked), RoutingStrategies.Bubble); RoutedEvent.Register<ToggleButton, RoutedEventArgs>(
nameof(Checked),
RoutingStrategies.Bubble);
/// <summary> /// <summary>
/// Defines the <see cref="Unchecked"/> event. /// Defines the <see cref="Unchecked"/> event.
/// </summary> /// </summary>
[Obsolete("Use IsCheckedChangedEvent instead.")]
public static readonly RoutedEvent<RoutedEventArgs> UncheckedEvent = public static readonly RoutedEvent<RoutedEventArgs> UncheckedEvent =
RoutedEvent.Register<ToggleButton, RoutedEventArgs>(nameof(Unchecked), RoutingStrategies.Bubble); RoutedEvent.Register<ToggleButton, RoutedEventArgs>(
nameof(Unchecked),
RoutingStrategies.Bubble);
/// <summary> /// <summary>
/// Defines the <see cref="Unchecked"/> event. /// Defines the <see cref="Unchecked"/> event.
/// </summary> /// </summary>
[Obsolete("Use IsCheckedChangedEvent instead.")]
public static readonly RoutedEvent<RoutedEventArgs> IndeterminateEvent = public static readonly RoutedEvent<RoutedEventArgs> IndeterminateEvent =
RoutedEvent.Register<ToggleButton, RoutedEventArgs>(nameof(Indeterminate), RoutingStrategies.Bubble); RoutedEvent.Register<ToggleButton, RoutedEventArgs>(
nameof(Indeterminate),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="IsCheckedChanged"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> IsCheckedChangedEvent =
RoutedEvent.Register<ToggleButton, RoutedEventArgs>(
nameof(IsCheckedChanged),
RoutingStrategies.Bubble);
private bool? _isChecked = false; private bool? _isChecked = false;
static ToggleButton() static ToggleButton()
{ {
IsCheckedProperty.Changed.AddClassHandler<ToggleButton>((x, e) => x.OnIsCheckedChanged(e));
} }
public ToggleButton() public ToggleButton()
@ -62,6 +78,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Raised when a <see cref="ToggleButton"/> is checked. /// Raised when a <see cref="ToggleButton"/> is checked.
/// </summary> /// </summary>
[Obsolete("Use IsCheckedChanged instead.")]
public event EventHandler<RoutedEventArgs>? Checked public event EventHandler<RoutedEventArgs>? Checked
{ {
add => AddHandler(CheckedEvent, value); add => AddHandler(CheckedEvent, value);
@ -71,6 +88,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Raised when a <see cref="ToggleButton"/> is unchecked. /// Raised when a <see cref="ToggleButton"/> is unchecked.
/// </summary> /// </summary>
[Obsolete("Use IsCheckedChanged instead.")]
public event EventHandler<RoutedEventArgs>? Unchecked public event EventHandler<RoutedEventArgs>? Unchecked
{ {
add => AddHandler(UncheckedEvent, value); add => AddHandler(UncheckedEvent, value);
@ -80,12 +98,22 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Raised when a <see cref="ToggleButton"/> is neither checked nor unchecked. /// Raised when a <see cref="ToggleButton"/> is neither checked nor unchecked.
/// </summary> /// </summary>
[Obsolete("Use IsCheckedChanged instead.")]
public event EventHandler<RoutedEventArgs>? Indeterminate public event EventHandler<RoutedEventArgs>? Indeterminate
{ {
add => AddHandler(IndeterminateEvent, value); add => AddHandler(IndeterminateEvent, value);
remove => RemoveHandler(IndeterminateEvent, value); remove => RemoveHandler(IndeterminateEvent, value);
} }
/// <summary>
/// Raised when the <see cref="IsChecked"/> property value changes.
/// </summary>
public event EventHandler<RoutedEventArgs>? IsCheckedChanged
{
add => AddHandler(IsCheckedChangedEvent, value);
remove => RemoveHandler(IsCheckedChangedEvent, value);
}
/// <summary> /// <summary>
/// Gets or sets whether the <see cref="ToggleButton"/> is checked. /// Gets or sets whether the <see cref="ToggleButton"/> is checked.
/// </summary> /// </summary>
@ -147,6 +175,7 @@ namespace Avalonia.Controls.Primitives
/// Called when <see cref="IsChecked"/> becomes true. /// Called when <see cref="IsChecked"/> becomes true.
/// </summary> /// </summary>
/// <param name="e">Event arguments for the routed event that is raised by the default implementation of this method.</param> /// <param name="e">Event arguments for the routed event that is raised by the default implementation of this method.</param>
[Obsolete("Use OnIsCheckedChanged instead.")]
protected virtual void OnChecked(RoutedEventArgs e) protected virtual void OnChecked(RoutedEventArgs e)
{ {
RaiseEvent(e); RaiseEvent(e);
@ -156,6 +185,7 @@ namespace Avalonia.Controls.Primitives
/// Called when <see cref="IsChecked"/> becomes false. /// Called when <see cref="IsChecked"/> becomes false.
/// </summary> /// </summary>
/// <param name="e">Event arguments for the routed event that is raised by the default implementation of this method.</param> /// <param name="e">Event arguments for the routed event that is raised by the default implementation of this method.</param>
[Obsolete("Use OnIsCheckedChanged instead.")]
protected virtual void OnUnchecked(RoutedEventArgs e) protected virtual void OnUnchecked(RoutedEventArgs e)
{ {
RaiseEvent(e); RaiseEvent(e);
@ -165,31 +195,51 @@ namespace Avalonia.Controls.Primitives
/// Called when <see cref="IsChecked"/> becomes null. /// Called when <see cref="IsChecked"/> becomes null.
/// </summary> /// </summary>
/// <param name="e">Event arguments for the routed event that is raised by the default implementation of this method.</param> /// <param name="e">Event arguments for the routed event that is raised by the default implementation of this method.</param>
[Obsolete("Use OnIsCheckedChanged instead.")]
protected virtual void OnIndeterminate(RoutedEventArgs e) protected virtual void OnIndeterminate(RoutedEventArgs e)
{ {
RaiseEvent(e); RaiseEvent(e);
} }
/// <summary>
/// Called when <see cref="IsChecked"/> changes.
/// </summary>
/// <param name="e">Event arguments for the routed event that is raised by the default implementation of this method.</param>
protected virtual void OnIsCheckedChanged(RoutedEventArgs e)
{
RaiseEvent(e);
}
protected override AutomationPeer OnCreateAutomationPeer() protected override AutomationPeer OnCreateAutomationPeer()
{ {
return new ToggleButtonAutomationPeer(this); return new ToggleButtonAutomationPeer(this);
} }
private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e) /// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
var newValue = (bool?)e.NewValue; base.OnPropertyChanged(change);
switch (newValue) if (change.Property == IsCheckedProperty)
{ {
case true: var newValue = change.GetNewValue<bool?>();
OnChecked(new RoutedEventArgs(CheckedEvent));
break; #pragma warning disable CS0618 // Type or member is obsolete
case false: switch (newValue)
OnUnchecked(new RoutedEventArgs(UncheckedEvent)); {
break; case true:
default: OnChecked(new RoutedEventArgs(CheckedEvent));
OnIndeterminate(new RoutedEventArgs(IndeterminateEvent)); break;
break; case false:
OnUnchecked(new RoutedEventArgs(UncheckedEvent));
break;
default:
OnIndeterminate(new RoutedEventArgs(IndeterminateEvent));
break;
}
#pragma warning restore CS0618 // Type or member is obsolete
OnIsCheckedChanged(new RoutedEventArgs(IsCheckedChangedEvent));
} }
} }

26
src/Avalonia.Controls/SplitButton/SplitButton.cs

@ -25,7 +25,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Raised when the user presses the primary part of the <see cref="SplitButton"/>. /// Raised when the user presses the primary part of the <see cref="SplitButton"/>.
/// </summary> /// </summary>
public event EventHandler<RoutedEventArgs> Click public event EventHandler<RoutedEventArgs>? Click
{ {
add => AddHandler(ClickEvent, value); add => AddHandler(ClickEvent, value);
remove => RemoveHandler(ClickEvent, value); remove => RemoveHandler(ClickEvent, value);
@ -35,7 +35,9 @@ namespace Avalonia.Controls
/// Defines the <see cref="Click"/> event. /// Defines the <see cref="Click"/> event.
/// </summary> /// </summary>
public static readonly RoutedEvent<RoutedEventArgs> ClickEvent = public static readonly RoutedEvent<RoutedEventArgs> ClickEvent =
RoutedEvent.Register<SplitButton, RoutedEventArgs>(nameof(Click), RoutingStrategies.Bubble); RoutedEvent.Register<SplitButton, RoutedEventArgs>(
nameof(Click),
RoutingStrategies.Bubble);
/// <summary> /// <summary>
/// Defines the <see cref="Command"/> property. /// Defines the <see cref="Command"/> property.
@ -69,10 +71,6 @@ namespace Avalonia.Controls
private IDisposable? _flyoutPropertyChangedDisposable; private IDisposable? _flyoutPropertyChangedDisposable;
////////////////////////////////////////////////////////////////////////
// Constructor / Destructors
////////////////////////////////////////////////////////////////////////
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SplitButton"/> class. /// Initializes a new instance of the <see cref="SplitButton"/> class.
/// </summary> /// </summary>
@ -80,10 +78,6 @@ namespace Avalonia.Controls
{ {
} }
////////////////////////////////////////////////////////////////////////
// Properties
////////////////////////////////////////////////////////////////////////
/// <summary> /// <summary>
/// Gets or sets the <see cref="ICommand"/> invoked when the primary part is pressed. /// Gets or sets the <see cref="ICommand"/> invoked when the primary part is pressed.
/// </summary> /// </summary>
@ -124,10 +118,6 @@ namespace Avalonia.Controls
/// <inheritdoc/> /// <inheritdoc/>
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
////////////////////////////////////////////////////////////////////////
// Methods
////////////////////////////////////////////////////////////////////////
/// <inheritdoc/> /// <inheritdoc/>
void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e);
@ -222,10 +212,6 @@ namespace Avalonia.Controls
} }
} }
////////////////////////////////////////////////////////////////////////
// OnEvent Overridable Methods
////////////////////////////////////////////////////////////////////////
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
@ -429,10 +415,6 @@ namespace Avalonia.Controls
// Available for derived types // Available for derived types
} }
////////////////////////////////////////////////////////////////////////
// Event Handling
////////////////////////////////////////////////////////////////////////
/// <summary> /// <summary>
/// Event handler for when the internal primary button part is pressed. /// Event handler for when the internal primary button part is pressed.
/// </summary> /// </summary>

18
src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs

@ -18,7 +18,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Raised when the <see cref="IsChecked"/> property value changes. /// Raised when the <see cref="IsChecked"/> property value changes.
/// </summary> /// </summary>
public event EventHandler<RoutedEventArgs> IsCheckedChanged public event EventHandler<RoutedEventArgs>? IsCheckedChanged
{ {
add => AddHandler(IsCheckedChangedEvent, value); add => AddHandler(IsCheckedChangedEvent, value);
remove => RemoveHandler(IsCheckedChangedEvent, value); remove => RemoveHandler(IsCheckedChangedEvent, value);
@ -39,10 +39,6 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<ToggleSplitButton, bool>( AvaloniaProperty.Register<ToggleSplitButton, bool>(
nameof(IsChecked)); nameof(IsChecked));
////////////////////////////////////////////////////////////////////////
// Constructor / Destructors
////////////////////////////////////////////////////////////////////////
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ToggleSplitButton"/> class. /// Initializes a new instance of the <see cref="ToggleSplitButton"/> class.
/// </summary> /// </summary>
@ -50,10 +46,6 @@ namespace Avalonia.Controls
{ {
} }
////////////////////////////////////////////////////////////////////////
// Properties
////////////////////////////////////////////////////////////////////////
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the <see cref="ToggleSplitButton"/> is checked. /// Gets or sets a value indicating whether the <see cref="ToggleSplitButton"/> is checked.
/// </summary> /// </summary>
@ -73,10 +65,6 @@ namespace Avalonia.Controls
/// </remarks> /// </remarks>
Type IStyleable.StyleKey => typeof(SplitButton); Type IStyleable.StyleKey => typeof(SplitButton);
////////////////////////////////////////////////////////////////////////
// Methods
////////////////////////////////////////////////////////////////////////
/// <summary> /// <summary>
/// Toggles the <see cref="IsChecked"/> property between true and false. /// Toggles the <see cref="IsChecked"/> property between true and false.
/// </summary> /// </summary>
@ -85,10 +73,6 @@ namespace Avalonia.Controls
IsChecked = !IsChecked; IsChecked = !IsChecked;
} }
////////////////////////////////////////////////////////////////////////
// OnEvent Overridable Methods
////////////////////////////////////////////////////////////////////////
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{ {

26
src/Avalonia.Controls/TextBlock.cs

@ -173,13 +173,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text. /// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
public TextLayout TextLayout public TextLayout TextLayout => _textLayout ??= CreateTextLayout(_text);
{
get
{
return _textLayout ??= CreateTextLayout(_text);
}
}
/// <summary> /// <summary>
/// Gets or sets the padding to place around the <see cref="Text"/>. /// Gets or sets the padding to place around the <see cref="Text"/>.
@ -648,6 +642,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
protected void InvalidateTextLayout() protected void InvalidateTextLayout()
{ {
_textLayout?.Dispose();
_textLayout = null; _textLayout = null;
InvalidateVisual(); InvalidateVisual();
@ -663,6 +658,7 @@ namespace Avalonia.Controls
_constraint = availableSize.Deflate(padding); _constraint = availableSize.Deflate(padding);
_textLayout?.Dispose();
_textLayout = null; _textLayout = null;
var inlines = Inlines; var inlines = Inlines;
@ -726,6 +722,7 @@ namespace Avalonia.Controls
_constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity); _constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity);
_textLayout?.Dispose();
_textLayout = null; _textLayout = null;
if (HasComplexContent) if (HasComplexContent)
@ -829,12 +826,12 @@ namespace Avalonia.Controls
protected readonly record struct SimpleTextSource : ITextSource protected readonly record struct SimpleTextSource : ITextSource
{ {
private readonly CharacterBufferRange _text; private readonly string _text;
private readonly TextRunProperties _defaultProperties; private readonly TextRunProperties _defaultProperties;
public SimpleTextSource(string text, TextRunProperties defaultProperties) public SimpleTextSource(string text, TextRunProperties defaultProperties)
{ {
_text = new CharacterBufferRange(new CharacterBufferReference(text), text.Length); _text = text;
_defaultProperties = defaultProperties; _defaultProperties = defaultProperties;
} }
@ -845,14 +842,14 @@ namespace Avalonia.Controls
return new TextEndOfParagraph(); return new TextEndOfParagraph();
} }
var runText = _text.Skip(textSourceIndex); var runText = _text.AsMemory(textSourceIndex);
if (runText.IsEmpty) if (runText.IsEmpty)
{ {
return new TextEndOfParagraph(); return new TextEndOfParagraph();
} }
return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultProperties); return new TextCharacters(runText, _defaultProperties);
} }
} }
@ -887,14 +884,9 @@ namespace Avalonia.Controls
if (textRun is TextCharacters) if (textRun is TextCharacters)
{ {
var characterBufferReference = textRun.CharacterBufferReference;
var skip = Math.Max(0, textSourceIndex - currentPosition); var skip = Math.Max(0, textSourceIndex - currentPosition);
return new TextCharacters( return new TextCharacters(textRun.Text.Slice(skip), textRun.Properties!);
new CharacterBufferReference(characterBufferReference.CharacterBuffer, characterBufferReference.OffsetToFirstChar + skip),
textRun.Length - skip,
textRun.Properties!);
} }
return textRun; return textRun;

6
src/Avalonia.Controls/TextBox.cs

@ -961,9 +961,7 @@ namespace Avalonia.Controls
var length = 0; var length = 0;
var inputRange = new CharacterBufferRange(new CharacterBufferReference(input), input.Length); var graphemeEnumerator = new GraphemeEnumerator(input.AsSpan());
var graphemeEnumerator = new GraphemeEnumerator(inputRange);
while (graphemeEnumerator.MoveNext()) while (graphemeEnumerator.MoveNext())
{ {
@ -981,7 +979,7 @@ namespace Avalonia.Controls
} }
} }
length += grapheme.Length; length += grapheme.Text.Length;
} }
if (length < input.Length) if (length < input.Length)

6
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -79,12 +79,10 @@ namespace Avalonia.Controls
{ {
if(run.Length > 0) if(run.Length > 0)
{ {
var characterBufferRange = new CharacterBufferRange(run.CharacterBufferReference, run.Length);
#if NET6_0 #if NET6_0
builder.Append(characterBufferRange.Span); builder.Append(run.Text.Span);
#else #else
builder.Append(characterBufferRange.Span.ToArray()); builder.Append(run.Text.Span.ToArray());
#endif #endif
} }
} }

72
src/Avalonia.Controls/TopLevel.cs

@ -7,6 +7,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
@ -17,6 +18,8 @@ using Avalonia.Rendering;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Input.Platform;
using System.Linq;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -76,6 +79,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty = public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty =
AvaloniaProperty.Register<TopLevel, IBrush>(nameof(TransparencyBackgroundFallback), Brushes.White); AvaloniaProperty.Register<TopLevel, IBrush>(nameof(TransparencyBackgroundFallback), Brushes.White);
/// <summary>
/// Defines the <see cref="BackRequested"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> BackRequestedEvent =
RoutedEvent.Register<TopLevel, RoutedEventArgs>(nameof(BackRequested), RoutingStrategies.Bubble);
private static readonly WeakEvent<IResourceHost, ResourcesChangedEventArgs> private static readonly WeakEvent<IResourceHost, ResourcesChangedEventArgs>
ResourcesChangedWeakEvent = WeakEvent.Register<IResourceHost, ResourcesChangedEventArgs>( ResourcesChangedWeakEvent = WeakEvent.Register<IResourceHost, ResourcesChangedEventArgs>(
(s, h) => s.ResourcesChanged += h, (s, h) => s.ResourcesChanged += h,
@ -89,6 +98,7 @@ namespace Avalonia.Controls
private readonly IGlobalStyles? _globalStyles; private readonly IGlobalStyles? _globalStyles;
private readonly PointerOverPreProcessor? _pointerOverPreProcessor; private readonly PointerOverPreProcessor? _pointerOverPreProcessor;
private readonly IDisposable? _pointerOverPreProcessorSubscription; private readonly IDisposable? _pointerOverPreProcessorSubscription;
private readonly IDisposable? _backGestureSubscription;
private Size _clientSize; private Size _clientSize;
private Size? _frameSize; private Size? _frameSize;
private WindowTransparencyLevel _actualTransparencyLevel; private WindowTransparencyLevel _actualTransparencyLevel;
@ -205,6 +215,48 @@ namespace Avalonia.Controls
_pointerOverPreProcessor = new PointerOverPreProcessor(this); _pointerOverPreProcessor = new PointerOverPreProcessor(this);
_pointerOverPreProcessorSubscription = _inputManager?.PreProcess.Subscribe(_pointerOverPreProcessor); _pointerOverPreProcessorSubscription = _inputManager?.PreProcess.Subscribe(_pointerOverPreProcessor);
if(impl is ITopLevelWithSystemNavigationManager topLevelWithSystemNavigation)
{
topLevelWithSystemNavigation.SystemNavigationManager.BackRequested += (s, e) =>
{
e.RoutedEvent = BackRequestedEvent;
RaiseEvent(e);
};
}
_backGestureSubscription = _inputManager?.PreProcess.Subscribe(e =>
{
bool backRequested = false;
if (e is RawKeyEventArgs rawKeyEventArgs && rawKeyEventArgs.Type == RawKeyEventType.KeyDown)
{
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>()?.Back;
if (keymap != null)
{
var keyEvent = new KeyEventArgs()
{
KeyModifiers = (KeyModifiers)rawKeyEventArgs.Modifiers,
Key = rawKeyEventArgs.Key
};
backRequested = keymap.Any( key => key.Matches(keyEvent));
}
}
else if(e is RawPointerEventArgs pointerEventArgs)
{
backRequested = pointerEventArgs.Type == RawPointerEventType.XButton1Down;
}
if (backRequested)
{
var backRequestedEventArgs = new RoutedEventArgs(BackRequestedEvent);
RaiseEvent(backRequestedEventArgs);
e.Handled = backRequestedEventArgs.Handled;
}
});
} }
/// <summary> /// <summary>
@ -263,6 +315,15 @@ namespace Avalonia.Controls
set => SetValue(TransparencyBackgroundFallbackProperty, value); set => SetValue(TransparencyBackgroundFallbackProperty, value);
} }
/// <summary>
/// Occurs when physical Back Button is pressed or a back navigation has been requested.
/// </summary>
public event EventHandler<RoutedEvent> BackRequested
{
add { AddHandler(BackRequestedEvent, value); }
remove { RemoveHandler(BackRequestedEvent, value); }
}
public ILayoutManager LayoutManager public ILayoutManager LayoutManager
{ {
get get
@ -341,6 +402,16 @@ namespace Avalonia.Controls
{ {
return PlatformImpl?.PointToScreen(p) ?? default; return PlatformImpl?.PointToScreen(p) ?? default;
} }
/// <summary>
/// Gets the <see cref="TopLevel" /> for which the given <see cref="Visual"/> is hosted in.
/// </summary>
/// <param name="visual">The visual to query its TopLevel</param>
/// <returns>The TopLevel</returns>
public static TopLevel? GetTopLevel(Visual? visual)
{
return visual == null ? null : visual.VisualRoot as TopLevel;
}
/// <summary> /// <summary>
/// Creates the layout manager for this <see cref="TopLevel" />. /// Creates the layout manager for this <see cref="TopLevel" />.
@ -372,6 +443,7 @@ namespace Avalonia.Controls
_pointerOverPreProcessor?.OnCompleted(); _pointerOverPreProcessor?.OnCompleted();
_pointerOverPreProcessorSubscription?.Dispose(); _pointerOverPreProcessorSubscription?.Dispose();
_backGestureSubscription?.Dispose();
PlatformImpl = null; PlatformImpl = null;

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

@ -182,7 +182,9 @@ namespace Avalonia.DesignerSupport.Remote
public bool IsClientAreaExtendedToDecorations { get; } public bool IsClientAreaExtendedToDecorations { get; }
public bool NeedsManagedDecorations => false; public bool NeedsManagedDecorations => false;
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);
} }

4
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -25,9 +25,9 @@ namespace Avalonia.FreeDesktop
return new DBusMenuExporterImpl(DBusHelper.Connection, xid); return new DBusMenuExporterImpl(DBusHelper.Connection, xid);
} }
public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConection) public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConnection)
{ {
return new DBusMenuExporterImpl(currentConection, path); return new DBusMenuExporterImpl(currentConnection, path);
} }
public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/"

77
src/Avalonia.FreeDesktop/DBusPlatformSettings.cs

@ -0,0 +1,77 @@
using System;
using System.Threading.Tasks;
using Avalonia.Logging;
using Avalonia.Platform;
namespace Avalonia.FreeDesktop;
internal class DBusPlatformSettings : DefaultPlatformSettings
{
private readonly IDBusSettings? _settings;
private PlatformColorValues? _lastColorValues;
public DBusPlatformSettings()
{
_settings = DBusHelper.TryInitialize()?
.CreateProxy<IDBusSettings>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
if (_settings is not null)
{
_ = _settings.WatchSettingChangedAsync(SettingsChangedHandler);
_ = TryGetInitialValue();
}
}
public override PlatformColorValues GetColorValues()
{
return _lastColorValues ?? base.GetColorValues();
}
private async Task TryGetInitialValue()
{
var colorSchemeTask = _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme");
if (colorSchemeTask.Status == TaskStatus.RanToCompletion)
{
_lastColorValues = GetColorValuesFromSetting(colorSchemeTask.Result);
}
else
{
try
{
var value = await colorSchemeTask;
_lastColorValues = GetColorValuesFromSetting(value);
OnColorValuesChanged(_lastColorValues);
}
catch (Exception ex)
{
_lastColorValues = base.GetColorValues();
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get setting value", ex);
}
}
}
private void SettingsChangedHandler((string @namespace, string key, object value) tuple)
{
if (tuple.@namespace == "org.freedesktop.appearance"
&& tuple.key == "color-scheme")
{
/*
<member>0: No preference</member>
<member>1: Prefer dark appearance</member>
<member>2: Prefer light appearance</member>
*/
_lastColorValues = GetColorValuesFromSetting(tuple.value);
OnColorValuesChanged(_lastColorValues);
}
}
private static PlatformColorValues GetColorValuesFromSetting(object value)
{
var isDark = value?.ToString() == "1";
return new PlatformColorValues
{
ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light
};
}
}

16
src/Avalonia.FreeDesktop/DBusSettings.cs

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Tmds.DBus;
namespace Avalonia.FreeDesktop;
[DBusInterface("org.freedesktop.portal.Settings")]
internal interface IDBusSettings : IDBusObject
{
Task<(string @namespace, IDictionary<string, object>)> ReadAllAsync(string[] namespaces);
Task<object> ReadAsync(string @namespace, string key);
Task<IDisposable> WatchSettingChangedAsync(Action<(string @namespace, string key, object value)> handler, Action<Exception>? onError = null);
}

4
src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs

@ -1,14 +1,10 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Reactive;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia.FreeDesktop namespace Avalonia.FreeDesktop

5
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -128,6 +128,11 @@ namespace Avalonia.Headless
public void Dispose() public void Dispose()
{ {
} }
public IReadOnlyList<float> GetIntersections(float lowerBound, float upperBound)
{
throw new NotImplementedException();
}
} }
class HeadlessGeometryStub : IGeometryImpl class HeadlessGeometryStub : IGeometryImpl

6
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -145,15 +145,13 @@ namespace Avalonia.Headless
class HeadlessTextShaperStub : ITextShaperImpl class HeadlessTextShaperStub : ITextShaperImpl
{ {
public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options) public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
{ {
var typeface = options.Typeface; var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize; var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel; var bidiLevel = options.BidiLevel;
var characterBufferRange = new CharacterBufferRange(text, length); return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
return new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel);
} }
} }

5
src/Avalonia.Headless/HeadlessWindowImpl.cs

@ -345,5 +345,10 @@ namespace Avalonia.Headless
{ {
} }
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
}
} }
} }

3
src/Avalonia.Native/AvaloniaNativeDragSource.cs

@ -6,7 +6,6 @@ using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Native.Interop; using Avalonia.Native.Interop;
using Avalonia.VisualTree;
namespace Avalonia.Native namespace Avalonia.Native
{ {
@ -26,7 +25,7 @@ namespace Avalonia.Native
if (element == null) if (element == null)
return null; return null;
var visual = (Visual)element; var visual = (Visual)element;
return visual.GetVisualRoot() as TopLevel; return TopLevel.GetTopLevel(visual);
} }
class DndCallback : NativeCallbackBase, IAvnDndResultCallback class DndCallback : NativeCallbackBase, IAvnDndResultCallback

2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -104,7 +104,7 @@ namespace Avalonia.Native
.Bind<ICursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind<ICursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
.Bind<IPlatformIconLoader>().ToSingleton<IconLoader>() .Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice) .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>() .Bind<IPlatformSettings>().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings()))
.Bind<IWindowingPlatform>().ToConstant(this) .Bind<IWindowingPlatform>().ToConstant(this)
.Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) .Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60)) .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))

1
src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs

@ -3,7 +3,6 @@ using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Reactive;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Threading; using Avalonia.Threading;

77
src/Avalonia.Native/NativePlatformSettings.cs

@ -0,0 +1,77 @@
using System;
using Avalonia.Media;
using Avalonia.Native.Interop;
using Avalonia.Platform;
namespace Avalonia.Native;
internal class NativePlatformSettings : DefaultPlatformSettings
{
private readonly IAvnPlatformSettings _platformSettings;
private PlatformColorValues _lastColorValues;
public NativePlatformSettings(IAvnPlatformSettings platformSettings)
{
_platformSettings = platformSettings;
platformSettings.RegisterColorsChange(new ColorsChangeCallback(this));
}
public override PlatformColorValues GetColorValues()
{
var (theme, contrast) = _platformSettings.PlatformTheme switch
{
AvnPlatformThemeVariant.Dark => (PlatformThemeVariant.Dark, ColorContrastPreference.NoPreference),
AvnPlatformThemeVariant.Light => (PlatformThemeVariant.Light, ColorContrastPreference.NoPreference),
AvnPlatformThemeVariant.HighContrastDark => (PlatformThemeVariant.Dark, ColorContrastPreference.High),
AvnPlatformThemeVariant.HighContrastLight => (PlatformThemeVariant.Dark, ColorContrastPreference.High),
_ => throw new ArgumentOutOfRangeException()
};
var color = _platformSettings.AccentColor;
if (color > 0)
{
_lastColorValues = new PlatformColorValues
{
ThemeVariant = theme,
ContrastPreference = contrast,
AccentColor1 = Color.FromUInt32(color)
};
}
else
{
_lastColorValues = new PlatformColorValues
{
ThemeVariant = theme,
ContrastPreference = contrast
};
}
return _lastColorValues;
}
public void OnColorValuesChanged()
{
var oldColorValues = _lastColorValues;
var colorValues = GetColorValues();
if (oldColorValues != colorValues)
{
OnColorValuesChanged(colorValues);
}
}
private class ColorsChangeCallback : NativeCallbackBase, IAvnActionCallback
{
private readonly NativePlatformSettings _settings;
public ColorsChangeCallback(NativePlatformSettings settings)
{
_settings = settings;
}
public void Run()
{
_settings.OnColorValuesChanged();
}
}
}

5
src/Avalonia.Native/WindowImplBase.cs

@ -519,6 +519,11 @@ namespace Avalonia.Native
public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.Transparent; public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.Transparent;
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
_native.SetFrameThemeVariant((AvnPlatformThemeVariant)themeVariant);
}
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0, 0); public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0, 0);
public IPlatformHandle Handle { get; private set; } public IPlatformHandle Handle { get; private set; }

18
src/Avalonia.Native/avn.idl

@ -473,6 +473,14 @@ enum AvnWindowTransparencyMode
Blur Blur
} }
enum AvnPlatformThemeVariant
{
Light,
Dark,
HighContrastLight,
HighContrastDark,
}
[uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]
interface IAvaloniaNativeFactory : IUnknown interface IAvaloniaNativeFactory : IUnknown
{ {
@ -494,6 +502,7 @@ interface IAvaloniaNativeFactory : IUnknown
HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv);
HRESULT CreateTrayIcon(IAvnTrayIcon** ppv); HRESULT CreateTrayIcon(IAvnTrayIcon** ppv);
HRESULT CreateApplicationCommands(IAvnApplicationCommands** ppv); HRESULT CreateApplicationCommands(IAvnApplicationCommands** ppv);
HRESULT CreatePlatformSettings(IAvnPlatformSettings** ppv);
} }
[uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)]
@ -535,6 +544,7 @@ interface IAvnWindowBase : IUnknown
HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard* clipboard, IAvnDndResultCallback* cb, [intptr]void* sourceHandle); IAvnClipboard* clipboard, IAvnDndResultCallback* cb, [intptr]void* sourceHandle);
HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode); HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode);
HRESULT SetFrameThemeVariant(AvnPlatformThemeVariant mode);
} }
[uuid(83e588f3-6981-4e48-9ea0-e1e569f79a91), cpp-virtual-inherits] [uuid(83e588f3-6981-4e48-9ea0-e1e569f79a91), cpp-virtual-inherits]
@ -906,3 +916,11 @@ interface IAvnAutomationNode : IUnknown
void PropertyChanged(AvnAutomationProperty property); void PropertyChanged(AvnAutomationProperty property);
void FocusChanged(); void FocusChanged();
} }
[uuid(d1f009cc-9d2d-493b-845d-90d2c104baae)]
interface IAvnPlatformSettings : IUnknown
{
AvnPlatformThemeVariant GetPlatformTheme();
uint GetAccentColor();
void RegisterColorsChange(IAvnActionCallback* callback);
}

2
src/Avalonia.X11/X11Platform.cs

@ -80,7 +80,7 @@ namespace Avalonia.X11
.Bind<IKeyboardDevice>().ToFunc(() => KeyboardDevice) .Bind<IKeyboardDevice>().ToFunc(() => KeyboardDevice)
.Bind<ICursorFactory>().ToConstant(new X11CursorFactory(Display)) .Bind<ICursorFactory>().ToConstant(new X11CursorFactory(Display))
.Bind<IClipboard>().ToConstant(new X11Clipboard(this)) .Bind<IClipboard>().ToConstant(new X11Clipboard(this))
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>() .Bind<IPlatformSettings>().ToSingleton<DBusPlatformSettings>()
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader()) .Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider()) .Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this)); .Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this));

2
src/Avalonia.X11/X11Window.cs

@ -1226,6 +1226,8 @@ namespace Avalonia.X11
public WindowTransparencyLevel TransparencyLevel => public WindowTransparencyLevel TransparencyLevel =>
_transparencyHelper?.CurrentLevel ?? WindowTransparencyLevel.None; _transparencyHelper?.CurrentLevel ?? WindowTransparencyLevel.None;
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8); public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8);
public bool NeedsManagedDecorations => false; public bool NeedsManagedDecorations => false;

31
src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs

@ -0,0 +1,31 @@
using Avalonia.Browser.Interop;
using Avalonia.Platform;
namespace Avalonia.Browser;
internal class BrowserPlatformSettings : DefaultPlatformSettings
{
private bool _isDarkMode;
private bool _isHighContrast;
public BrowserPlatformSettings()
{
var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) =>
{
_isDarkMode = isDarkMode;
_isHighContrast = isHighContrast;
OnColorValuesChanged(GetColorValues());
});
_isDarkMode = obj.GetPropertyAsBoolean("isDarkMode");
_isHighContrast = obj.GetPropertyAsBoolean("isHighContrast");
}
public override PlatformColorValues GetColorValues()
{
return base.GetColorValues() with
{
ThemeVariant = _isDarkMode ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light,
ContrastPreference = _isHighContrast ? ColorContrastPreference.High : ColorContrastPreference.NoPreference
};
}
}

24
src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs

@ -0,0 +1,24 @@
using System;
using Avalonia.Browser.Interop;
using Avalonia.Interactivity;
using Avalonia.Platform;
namespace Avalonia.Browser
{
internal class BrowserSystemNavigationManager : ISystemNavigationManager
{
public event EventHandler<RoutedEventArgs>? BackRequested;
public BrowserSystemNavigationManager()
{
NavigationHelper.AddBackHandler(() =>
{
var routedEventArgs = new RoutedEventArgs();
BackRequested?.Invoke(this, routedEventArgs);
return routedEventArgs.Handled;
});
}
}
}

10
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@ -19,7 +19,8 @@ using Avalonia.Rendering.Composition;
namespace Avalonia.Browser namespace Avalonia.Browser
{ {
internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider,
ITopLevelWithSystemNavigationManager
{ {
private Size _clientSize; private Size _clientSize;
private IInputRoot? _inputRoot; private IInputRoot? _inputRoot;
@ -228,11 +229,18 @@ namespace Avalonia.Browser
public IKeyboardDevice KeyboardDevice { get; } = BrowserWindowingPlatform.Keyboard; public IKeyboardDevice KeyboardDevice { get; } = BrowserWindowingPlatform.Keyboard;
public WindowTransparencyLevel TransparencyLevel { get; private set; } public WindowTransparencyLevel TransparencyLevel { get; private set; }
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
// not in the standard, but we potentially can use "apple-mobile-web-app-status-bar-style" for iOS and "theme-color" for android.
}
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; }
public ITextInputMethodImpl TextInputMethod => _avaloniaView; public ITextInputMethodImpl TextInputMethod => _avaloniaView;
public INativeControlHostImpl? NativeControlHost { get; } public INativeControlHostImpl? NativeControlHost { get; }
public IStorageProvider StorageProvider { get; } = new BrowserStorageProvider(); public IStorageProvider StorageProvider { get; } = new BrowserStorageProvider();
public ISystemNavigationManager SystemNavigationManager { get; } = new BrowserSystemNavigationManager();
} }
} }

5
src/Browser/Avalonia.Browser/Interop/DomHelper.cs

@ -25,4 +25,9 @@ internal static partial class DomHelper
public static partial double ObserveDpi( public static partial double ObserveDpi(
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>] [JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>]
Action<double, double> onDpiChanged); Action<double, double> onDpiChanged);
[JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)]
public static partial JSObject ObserveDarkMode(
[JSMarshalAs<JSType.Function<JSType.Boolean, JSType.Boolean>>]
Action<bool, bool> observer);
} }

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

Loading…
Cancel
Save