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 */; };
BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; };
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 */
/* Begin PBXFileReference section */
@ -103,6 +104,7 @@
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@ -163,6 +165,7 @@
1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */,
37A517B22159597E00FBA241 /* Screens.mm */,
37C09D8721580FE4006A6758 /* SystemDialogs.mm */,
EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */,
AB7A61F02147C815003C5833 /* Products */,
AB661C1C2148230E00291242 /* Frameworks */,
18391676ECF0E983F4964357 /* WindowBaseImpl.mm */,
@ -299,6 +302,7 @@
1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */,
18391AC16726CBC45856233B /* AvnWindow.mm in Sources */,
18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */,
EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

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

@ -44,7 +44,7 @@
-(bool) isDialog
{
return _parent->IsDialog();
return _parent->IsModal();
}
-(double) getExtendedTitleBarHeight
@ -281,6 +281,9 @@
- (void)windowDidBecomeKey:(NSNotification *_Nonnull)notification
{
if (_parent == nullptr)
return;
_parent->BringToFront();
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 SetFrameThemeVariant(AvnPlatformThemeVariant variant) override;
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard *clipboard, IAvnDndResultCallback *cb,
void *sourceHandle) override;
virtual bool IsDialog();
virtual bool IsModal();
id<AvnWindowProtocol> GetWindowProtocol ();

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

@ -498,6 +498,24 @@ HRESULT WindowBaseImpl::SetTransparencyMode(AvnWindowTransparencyMode mode) {
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) {
START_COM_CALL;
@ -542,7 +560,7 @@ HRESULT WindowBaseImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, Av
return S_OK;
}
bool WindowBaseImpl::IsDialog() {
bool WindowBaseImpl::IsModal() {
return false;
}

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

@ -23,7 +23,7 @@ private:
NSRect _preZoomSize;
bool _transitioningWindowState;
bool _isClientAreaExtended;
bool _isDialog;
bool _isModal;
WindowImpl* _parent;
std::list<WindowImpl*> _children;
AvnExtendClientAreaChromeHints _extendClientHints;
@ -91,7 +91,9 @@ BEGIN_INTERFACE_MAP()
virtual HRESULT SetWindowState (AvnWindowState state) override;
virtual bool IsDialog() override;
virtual bool IsModal() override;
bool IsOwned();
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;
@autoreleasepool {
_isDialog = isDialog || _parent != nullptr;
_isModal = isDialog;
WindowBaseImpl::Show(activate, isDialog);
@ -97,7 +97,7 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) {
_parent = cparent;
_isDialog = _parent != nullptr;
_isModal = _parent != 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
@ -123,7 +123,7 @@ void WindowImpl::BringToFront()
{
if ([Window isVisible] && ![Window isMiniaturized])
{
if(IsDialog())
if(IsModal())
{
Activate();
}
@ -150,7 +150,7 @@ bool WindowImpl::CanBecomeKeyWindow()
{
for(auto iterator = _children.begin(); iterator != _children.end(); iterator++)
{
if((*iterator)->IsDialog())
if((*iterator)->IsModal())
{
return false;
}
@ -569,8 +569,12 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) {
}
}
bool WindowImpl::IsDialog() {
return _isDialog;
bool WindowImpl::IsModal() {
return _isModal;
}
bool WindowImpl::IsOwned() {
return _parent != nullptr;
}
NSWindowStyleMask WindowImpl::GetStyle() {
@ -599,7 +603,7 @@ NSWindowStyleMask WindowImpl::GetStyle() {
break;
}
if (!IsDialog()) {
if (!IsOwned()) {
s |= NSWindowStyleMaskMiniaturizable;
}

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

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

25
packages/Avalonia/AvaloniaBuildTasks.targets

@ -48,8 +48,12 @@
</PropertyGroup>
<Target Name="_GenerateAvaloniaResourcesDependencyCache" BeforeTargets="GenerateAvaloniaResources">
<PropertyGroup>
<_AvaloniaResourcesInputsCacheFilePath>$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache</_AvaloniaResourcesInputsCacheFilePath>
</PropertyGroup>
<ItemGroup>
<CustomAdditionalGenerateAvaloniaResourcesInputs Include="$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache" />
<CustomAdditionalGenerateAvaloniaResourcesInputs Include="$(_AvaloniaResourcesInputsCacheFilePath)" />
</ItemGroup>
<Hash ItemsToHash="@(AvaloniaResource);@(AvaloniaXaml);$(MSBuildAllProjects)">
@ -57,7 +61,11 @@
</Hash>
<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 Name="GenerateAvaloniaResources"
@ -68,7 +76,7 @@
Condition="('@(AvaloniaResource->Count())' &gt; 0) or ('@(AvaloniaXaml->Count())' &gt; 0)"
>
<ItemGroup>
<AvaloniaResource Include="@(AvaloniaXaml)"/>
<AvaloniaResource Include="@(AvaloniaXaml)" />
</ItemGroup>
<GenerateAvaloniaResourcesTask
Condition="'$(_AvaloniaUseExternalMSBuild)' != 'true'"
@ -76,6 +84,9 @@
Root="$(MSBuildProjectDirectory)"
Resources="@(AvaloniaResource)"
ReportImportance="$(AvaloniaXamlReportImportance)"/>
<ItemGroup Condition="'$(_AvaloniaUseExternalMSBuild)' != 'true'">
<FileWrites Include="$(AvaloniaResourcesTemporaryFilePath)" />
</ItemGroup>
<Exec
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"/>
@ -103,6 +114,9 @@
File="$(AvaloniaXamlReferencesTemporaryFilePath)"
Lines="@(ReferencePathWithRefAssemblies)"
Overwrite="true" />
<ItemGroup Condition="'$(_AvaloniaForceInternalMSBuild)' != 'true'">
<FileWrites Include="$(AvaloniaXamlReferencesTemporaryFilePath)" />
</ItemGroup>
<CompileAvaloniaXamlTask
Condition="'$(_AvaloniaUseExternalMSBuild)' != 'true'"
AssemblyFile="@(IntermediateAssembly)"
@ -117,8 +131,9 @@
DelaySign="$(DelaySign)"
SkipXamlCompilation="$(_AvaloniaSkipXamlCompilation)"
DebuggerLaunch="$(AvaloniaXamlIlDebuggerLaunch)"
DefaultCompileBindings="$(AvaloniaUseCompiledBindingsByDefault)"
/>
DefaultCompileBindings="$(AvaloniaUseCompiledBindingsByDefault)">
<Output TaskParameter="WrittenFilePaths" ItemName="FileWrites" />
</CompileAvaloniaXamlTask>
<Exec
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"/>

73
samples/ControlCatalog/MainView.xaml.cs

@ -3,9 +3,11 @@ using System.Collections;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.VisualTree;
using ControlCatalog.Models;
using ControlCatalog.Pages;
@ -14,10 +16,14 @@ namespace ControlCatalog
{
public class MainView : UserControl
{
private readonly IPlatformSettings _platformSettings;
public MainView()
{
AvaloniaXamlLoader.Load(this);
_platformSettings = AvaloniaLocator.Current.GetRequiredService<IPlatformSettings>();
PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues());
var sideBar = this.Get<TabControl>("Sidebar");
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
@ -37,6 +43,15 @@ namespace ControlCatalog
if (themes.SelectedItem is CatalogTheme 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");
if (VisualRoot is Window window)
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 Capital { get; private set; }
public StateData(string name, string abbreviatoin, string capital)
public StateData(string name, string abbreviation, string capital)
{
Name = name;
Abbreviation = abbreviatoin;
Abbreviation = abbreviation;
Capital = capital;
}
@ -17,4 +17,4 @@ public class StateData
{
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;
}
Window GetWindow() => this.VisualRoot as Window ?? throw new NullReferenceException("Invalid Owner");
TopLevel GetTopLevel() => this.VisualRoot as TopLevel ?? throw new NullReferenceException("Invalid Owner");
Window GetWindow() => TopLevel.GetTopLevel(this) as Window ?? throw new NullReferenceException("Invalid Owner");
TopLevel GetTopLevel() => TopLevel.GetTopLevel(this) ?? throw new NullReferenceException("Invalid Owner");
private void InitializeComponent()
{

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

@ -27,7 +27,7 @@ namespace ControlCatalog.Pages
{
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<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IPlatformSettings>().ToSingleton<AndroidPlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())

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

@ -1,18 +1,14 @@
using System;
using Android.App;
using Android.Content;
using Android.Content.Res;
using Android.OS;
using Android.Runtime;
using Android.Views;
using AndroidX.AppCompat.App;
using AndroidX.Lifecycle;
using AndroidRect = Android.Graphics.Rect;
namespace Avalonia.Android
{
public abstract class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler
public abstract class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler, IActivityNavigationService
{
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()

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

@ -1,11 +1,14 @@
using System;
using Android.Content;
using Android.Content.Res;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Avalonia.Android.Platform;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
using Avalonia.Platform;
using Avalonia.Rendering;
namespace Avalonia.Android
@ -26,6 +29,7 @@ namespace Avalonia.Android
_root.Prepare();
this.SetBackgroundColor(global::Android.Graphics.Color.Transparent);
OnConfigurationChanged();
}
internal TopLevelImpl TopLevelImpl => _view;
@ -70,6 +74,18 @@ namespace Avalonia.Android
_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
{

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 Math = System.Math;
using AndroidRect = Android.Graphics.Rect;
using Window = Android.Views.Window;
using Android.Graphics.Drawables;
namespace Avalonia.Android.Platform.SkiaPlatform
{
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo,
ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider,
ITopLevelWithSystemNavigationManager
{
private readonly IGlPlatformSurface _gl;
private readonly IFramebufferPlatformSurface _framebuffer;
@ -57,6 +59,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
StorageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
SystemNavigationManager = new AndroidSystemNavigationManager(avaloniaView.Context as IActivityNavigationService);
}
public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
@ -286,6 +290,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
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);
IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle;
@ -300,6 +309,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IStorageProvider StorageProvider { get; }
public ISystemNavigationManager SystemNavigationManager { get; }
public void SetTransparencyLevelHint(WindowTransparencyLevel 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 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
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)
};
Back = new List<KeyGesture>
{
new KeyGesture(Key.Left, KeyModifiers.Alt)
};
}
public KeyModifiers CommandModifiers { get; set; }
@ -101,5 +105,6 @@ namespace Avalonia.Input.Platform
public List<KeyGesture> MoveCursorToTheStartOfDocumentWithSelection { get; set; }
public List<KeyGesture> MoveCursorToTheEndOfDocumentWithSelection { 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 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;
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 (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 (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 text = _that._text.AsMemory(textSourceCharacterIndex, thatFormatRider.Length);
TextRunProperties properties = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
var textCharacters = new TextCharacters(_that._text, textSourceCharacterIndex, thatFormatRider.Length,
properties);
return textCharacters;
return new TextCharacters(text, properties);
}
}
}

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

@ -1,12 +1,16 @@
using System;
using Avalonia.Platform;
using System.ComponentModel;
using System.Globalization;
using Avalonia.Reactive;
namespace Avalonia.Media
{
/// <summary>
/// Defines a geometric shape.
/// </summary>
/// </summary>
[TypeConverter(typeof(GeometryTypeConverter))]
public abstract class Geometry : AvaloniaObject
{
/// <summary>
@ -201,4 +205,32 @@ namespace Avalonia.Media
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 GlyphRunMetrics? _glyphRunMetrics;
private IReadOnlyList<char> _characters;
private ReadOnlyMemory<char> _characters;
private IReadOnlyList<ushort> _glyphIndices;
private IReadOnlyList<double>? _glyphAdvances;
private IReadOnlyList<Vector>? _glyphOffsets;
@ -41,7 +41,7 @@ namespace Avalonia.Media
public GlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
IReadOnlyList<char> characters,
ReadOnlyMemory<char> characters,
IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double>? glyphAdvances = null,
IReadOnlyList<Vector>? glyphOffsets = null,
@ -141,7 +141,7 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
/// </summary>
public IReadOnlyList<char> Characters
public ReadOnlyMemory<char> Characters
{
get => _characters;
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
{
@ -653,10 +653,10 @@ namespace Avalonia.Media
}
else
{
if (Characters != null && Characters.Count > 0)
if (!Characters.IsEmpty)
{
firstCluster = 0;
lastCluster = Characters.Count - 1;
lastCluster = Characters.Length - 1;
}
}
@ -716,14 +716,15 @@ namespace Avalonia.Media
glyphCount = 0;
newLineLength = 0;
var trailingWhitespaceLength = 0;
var charactersSpan = _characters.Span;
if (Characters != null)
if (!charactersSpan.IsEmpty)
{
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)
{
@ -743,55 +744,52 @@ namespace Avalonia.Media
}
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--)
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
characterIndex -= characterLength;
characterIndex -= characterLength;
if (!codepoint.IsWhiteSpace)
{
break;
}
if (!codepoint.IsWhiteSpace)
{
break;
}
var clusterLength = 1;
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++;
i--;
if(characterIndex >= 0)
{
codepoint = Codepoint.ReadAt(_characters, characterIndex, out characterLength);
codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
characterIndex -= characterLength;
}
continue;
characterIndex -= characterLength;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
continue;
}
trailingWhitespaceLength += clusterLength;
break;
}
glyphCount++;
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
glyphCount++;
}
}
}
@ -804,14 +802,15 @@ namespace Avalonia.Media
glyphCount = 0;
newLineLength = 0;
var trailingWhitespaceLength = 0;
var charactersSpan = Characters.Span;
if (Characters != null)
if (!charactersSpan.IsEmpty)
{
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)
{
@ -836,7 +835,7 @@ namespace Avalonia.Media
for (var i = 0; i < GlyphClusters.Count; 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;
@ -918,7 +917,7 @@ namespace Avalonia.Media
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
}
void IDisposable.Dispose()
public void 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.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media
{
@ -209,6 +214,51 @@ namespace Avalonia.Media
var pen = new Pen(Stroke ?? defaultBrush, thickness,
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));
}
}

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
{
private readonly CharacterBufferRange _text;
private readonly string _text;
private readonly TextRunProperties _defaultProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier;
public FormattedTextSource(string text, TextRunProperties defaultProperties,
IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
_text = new CharacterBufferRange(text);
_text = text;
_defaultProperties = defaultProperties;
_textModifier = textModifier;
}
@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting
return null;
}
var runText = _text.Skip(textSourceIndex);
var runText = _text.AsSpan(textSourceIndex);
if (runText.IsEmpty)
{
@ -35,7 +35,7 @@ namespace Avalonia.Media.TextFormatting
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>
@ -48,7 +48,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>
/// The created text style run.
/// </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)
{
if (textModifier == null || textModifier.Count == 0)
@ -122,7 +122,7 @@ namespace Avalonia.Media.TextFormatting
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;
@ -132,7 +132,7 @@ namespace Avalonia.Media.TextFormatting
{
var grapheme = graphemeEnumerator.Current;
finalLength += grapheme.Length;
finalLength += grapheme.Text.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)
{
var text = new CharacterBufferRange(textRun);
var text = textRun.Text;
if (text.IsEmpty)
{
continue;
}
var lineBreakEnumerator = new LineBreakEnumerator(text);
var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
while (lineBreakEnumerator.MoveNext())
{
@ -84,7 +84,7 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in lineImpl.TextRuns)
{
var text = textRun.CharacterBufferReference.CharacterBuffer;
var text = textRun.Text;
if (text.IsEmpty)
{

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

@ -1,31 +1,39 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using Avalonia.Utilities;
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();
public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
this(characterBufferRange, new GlyphInfo[bufferLength], glyphTypeface, fontRenderingEmSize, bidiLevel)
private bool _bufferRented;
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;
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel;
Length = GlyphInfos.Length;
}
internal ArraySlice<GlyphInfo> GlyphInfos { get; }
public int Length => GlyphInfos.Length;
public int Length { get; }
public IGlyphTypeface GlyphTypeface { get; }
@ -43,7 +51,7 @@ namespace Avalonia.Media.TextFormatting
public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
public CharacterBufferRange CharacterBufferRange { get; }
public ReadOnlyMemory<char> Text { get; }
/// <summary>
/// Finds a glyph index for given character index.
@ -105,7 +113,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The split result.</returns>
internal SplitResult<ShapedBuffer> Split(int length)
{
if (CharacterBufferRange.Length == length)
if (Text.Length == length)
{
return new SplitResult<ShapedBuffer>(this, null);
}
@ -117,10 +125,10 @@ namespace Avalonia.Media.TextFormatting
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);
var second = new ShapedBuffer(CharacterBufferRange.Skip(length),
var second = new ShapedBuffer(Text.Slice(length),
GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
return new SplitResult<ShapedBuffer>(first, second);
@ -260,6 +268,23 @@ namespace Avalonia.Media.TextFormatting
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

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

@ -6,15 +6,13 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// A text run that holds shaped characters.
/// </summary>
public sealed class ShapedTextRun : DrawableTextRun
public sealed class ShapedTextRun : DrawableTextRun, IDisposable
{
private GlyphRun? _glyphRun;
public ShapedTextRun(ShapedBuffer shapedBuffer, TextRunProperties properties)
{
ShapedBuffer = shapedBuffer;
CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference;
Length = shapedBuffer.CharacterBufferRange.Length;
Properties = properties;
TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize);
}
@ -26,13 +24,15 @@ namespace Avalonia.Media.TextFormatting
public ShapedBuffer ShapedBuffer { get; }
/// <inheritdoc/>
public override CharacterBufferReference CharacterBufferReference { get; }
public override ReadOnlyMemory<char> Text
=> ShapedBuffer.Text;
/// <inheritdoc/>
public override TextRunProperties Properties { get; }
/// <inheritdoc/>
public override int Length { get; }
public override int Length
=> ShapedBuffer.Text.Length;
public TextMetrics TextMetrics { get; }
@ -113,6 +113,7 @@ namespace Avalonia.Media.TextFormatting
{
length = 0;
var currentWidth = 0.0;
var charactersSpan = GlyphRun.Characters.Span;
for (var i = 0; i < ShapedBuffer.Length; i++)
{
@ -123,7 +124,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
Codepoint.ReadAt(GlyphRun.Characters, length, out var count);
Codepoint.ReadAt(charactersSpan, length, out var count);
length += count;
currentWidth += advance;
@ -136,6 +137,7 @@ namespace Avalonia.Media.TextFormatting
{
length = 0;
width = 0;
var charactersSpan = GlyphRun.Characters.Span;
for (var i = ShapedBuffer.Length - 1; i >= 0; i--)
{
@ -146,7 +148,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
Codepoint.ReadAt(GlyphRun.Characters, length, out var count);
Codepoint.ReadAt(charactersSpan, length, out var count);
length += count;
width += advance;
@ -192,12 +194,18 @@ namespace Avalonia.Media.TextFormatting
return new GlyphRun(
ShapedBuffer.GlyphTypeface,
ShapedBuffer.FontRenderingEmSize,
new CharacterBufferRange(CharacterBufferReference, Length),
Text,
ShapedBuffer.GlyphIndices,
ShapedBuffer.GlyphAdvances,
ShapedBuffer.GlyphOffsets,
ShapedBuffer.GlyphClusters,
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
{
/// <summary>
/// Construct a run of text content from character array
/// Constructs a run for text content from a string.
/// </summary>
public TextCharacters(
char[] characterArray,
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
)
{ }
public TextCharacters(string text, TextRunProperties textRunProperties)
: this(text.AsMemory(), textRunProperties)
{
}
/// <summary>
/// Internal constructor of TextContent
/// Constructs a run for text content from a memory region.
/// </summary>
public TextCharacters(
CharacterBufferReference characterBufferReference,
int length,
TextRunProperties textRunProperties
)
public TextCharacters(ReadOnlyMemory<char> text, TextRunProperties textRunProperties)
{
if (length <= 0)
{
throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero");
}
if (textRunProperties.FontRenderingEmSize <= 0)
{
throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero");
throw new ArgumentOutOfRangeException(nameof(textRunProperties), textRunProperties.FontRenderingEmSize,
$"Invalid {nameof(TextRunProperties.FontRenderingEmSize)}");
}
CharacterBufferReference = characterBufferReference;
Length = length;
Text = text;
Properties = textRunProperties;
}
/// <inheritdoc />
public override int Length { get; }
public override int Length
=> Text.Length;
/// <inheritdoc />
public override CharacterBufferReference CharacterBufferReference { get; }
public override ReadOnlyMemory<char> Text { get; }
/// <inheritdoc />
public override TextRunProperties Properties { get; }
@ -94,17 +46,19 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of <see cref="UnshapedTextRun"/>.
/// </summary>
/// <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 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);
characterBufferRange = characterBufferRange.Skip(shapeableRun.Length);
text = text.Slice(shapeableRun.Length);
previousProperties = shapeableRun.Properties;
}
@ -115,45 +69,46 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Creates a shapeable text run with unique properties.
/// </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="biDiLevel">The bidi level of the run.</param>
/// <param name="previousProperties"></param>
/// <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)
{
var defaultTypeface = defaultProperties.Typeface;
var currentTypeface = defaultTypeface;
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 (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);
}
}
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
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);
}
}
var codepoint = Codepoint.ReplacementCodepoint;
var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count));
var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span);
while (codepointEnumerator.MoveNext())
{
@ -173,10 +128,10 @@ namespace Avalonia.Media.TextFormatting
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface);
if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _))
if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _))
{
//Fallback found
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
@ -185,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
var glyphTypeface = currentTypeface.GlyphTypeface;
var enumerator = new GraphemeEnumerator(characterBufferRange);
var enumerator = new GraphemeEnumerator(textSpan);
while (enumerator.MoveNext())
{
@ -196,23 +151,23 @@ namespace Avalonia.Media.TextFormatting
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>
/// Tries to get a shapeable length that is supported by the specified typeface.
/// </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="defaultTypeface"></param>
/// <param name="length">The shapeable length.</param>
/// <param name="script"></param>
/// <returns></returns>
internal static bool TryGetShapeableLength(
CharacterBufferRange characterBufferRange,
ReadOnlySpan<char> text,
Typeface typeface,
Typeface? defaultTypeface,
out int length,
@ -221,7 +176,7 @@ namespace Avalonia.Media.TextFormatting
length = 0;
script = Script.Unknown;
if (characterBufferRange.Length == 0)
if (text.IsEmpty)
{
return false;
}
@ -229,7 +184,7 @@ namespace Avalonia.Media.TextFormatting
var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(characterBufferRange);
var enumerator = new GraphemeEnumerator(text);
while (enumerator.MoveNext())
{
@ -264,7 +219,7 @@ namespace Avalonia.Media.TextFormatting
}
}
length += currentGrapheme.Length;
length += currentGrapheme.Text.Length;
}
return length > 0;

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

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

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

@ -1,5 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
@ -158,25 +160,21 @@ namespace Avalonia.Media.TextFormatting
{
var flowDirection = paragraphProperties.FlowDirection;
var shapedRuns = new List<TextRun>();
var biDiData = new BidiData((sbyte)flowDirection);
using var biDiData = new BidiData((sbyte)flowDirection);
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 CharacterBufferRange(characterBuffer, textRun.Length));
biDiData.Append(new char[textRun.Length]);
}
else
{
var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
biDiData.Append(text);
biDiData.Append(textRun.Text.Span);
}
}
var biDi = new BidiAlgorithm();
using var biDi = new BidiAlgorithm();
biDi.Process(biDiData);
@ -187,10 +185,7 @@ namespace Avalonia.Media.TextFormatting
var processedRuns = new List<TextRun>(textRuns.Count);
foreach (var coalescedRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels))
{
processedRuns.AddRange(coalescedRuns);
}
CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns);
for (var index = 0; index < processedRuns.Count; index++)
{
@ -201,9 +196,7 @@ namespace Avalonia.Media.TextFormatting
case UnshapedTextRun shapeableRun:
{
var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun };
var characterBufferReference = currentRun.CharacterBufferReference;
var length = currentRun.Length;
var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar;
var text = shapeableRun.Text;
while (index + 1 < processedRuns.Count)
{
@ -212,23 +205,14 @@ namespace Avalonia.Media.TextFormatting
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);
length += nextRun.Length;
if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar)
{
offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar;
}
characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter);
index++;
shapeableRun = nextRun;
text = joinedText;
continue;
}
@ -240,7 +224,7 @@ namespace Avalonia.Media.TextFormatting
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
break;
}
@ -256,12 +240,81 @@ namespace Avalonia.Media.TextFormatting
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(
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 shapedBuffer = TextShaper.Current.ShapeText(text, length, options);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++)
{
@ -282,12 +335,14 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
/// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param>
/// <param name="levels">The bidi levels.</param>
/// <param name="processedRuns"></param>
/// <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)
{
yield break;
return;
}
var levelIndex = 0;
@ -295,7 +350,7 @@ namespace Avalonia.Media.TextFormatting
TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null;
CharacterBufferRange runText = default;
ReadOnlyMemory<char> runText = default;
for (var i = 0; i < textCharacters.Count; i++)
{
@ -306,18 +361,19 @@ namespace Avalonia.Media.TextFormatting
{
var drawableRun = textCharacters[i];
yield return new[] { drawableRun };
processedRuns.Add(drawableRun);
levelIndex += drawableRun.Length;
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)
{
@ -327,9 +383,9 @@ namespace Avalonia.Media.TextFormatting
levelIndex++;
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];
@ -342,9 +398,10 @@ namespace Avalonia.Media.TextFormatting
}
// 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;
@ -355,10 +412,10 @@ namespace Avalonia.Media.TextFormatting
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>
@ -412,7 +469,7 @@ namespace Avalonia.Media.TextFormatting
{
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);
textRuns.Add(splitResult);
@ -443,14 +500,14 @@ namespace Avalonia.Media.TextFormatting
{
lineBreak = default;
if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty)
var text = textRun.Text;
if (text.IsEmpty)
{
return false;
}
var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange);
var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
while (lineBreakEnumerator.MoveNext())
{
@ -542,8 +599,7 @@ namespace Avalonia.Media.TextFormatting
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length);
var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextRun(shapedBuffer, properties) };
@ -590,10 +646,8 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextRun:
{
var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
var lineBreaker = new LineBreakEnumerator(runText);
{
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (lineBreaker.MoveNext())
{
@ -652,9 +706,7 @@ namespace Avalonia.Media.TextFormatting
currentRun = textRuns[index];
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
lineBreaker = new LineBreakEnumerator(runText);
lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
}
}
else
@ -772,9 +824,7 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
var characterBuffer = textRun.CharacterBufferReference;
var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions);
var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
return new ShapedTextRun(shapedBuffer, textRun.Properties);
}

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

@ -8,7 +8,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Represents a multi line text layout.
/// </summary>
public class TextLayout
public class TextLayout : IDisposable
{
private readonly ITextSource _textSource;
private readonly TextParagraphProperties _paragraphProperties;
@ -566,5 +566,13 @@ namespace Avalonia.Media.TextFormatting
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>
/// Represents a line of text that is used for text rendering.
/// </summary>
public abstract class TextLine
public abstract class TextLine : IDisposable
{
/// <summary>
/// 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>
/// <returns>an array of bounding rectangles.</returns>
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.Collections.Generic;
using System.Linq;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
@ -978,6 +979,17 @@ namespace Avalonia.Media.TextFormatting
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()
{
_textLineMetrics = CreateLineMetrics();
@ -1436,6 +1448,13 @@ namespace Avalonia.Media.TextFormatting
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++)
{
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;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
@ -1490,7 +1509,7 @@ namespace Avalonia.Media.TextFormatting
{
case FlowDirection.LeftToRight:
{
if (index == _textRuns.Count - 1)
if (index == lastRunIndex)
{
width = widthIncludingWhitespace;
trailingWhitespaceLength = 0;
@ -1502,7 +1521,7 @@ namespace Avalonia.Media.TextFormatting
case FlowDirection.RightToLeft:
{
if (index == _textRuns.Count - 1)
if (index == lastRunIndex)
{
width = widthIncludingWhitespace;
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
{
@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Gets the text run's text.
/// </summary>
public virtual CharacterBufferReference CharacterBufferReference => default;
public virtual ReadOnlyMemory<char> Text => default;
/// <summary>
/// A set of properties shared by every characters in the run
@ -34,21 +35,7 @@ namespace Avalonia.Media.TextFormatting
_textRun = textRun;
}
public string Text
{
get
{
unsafe
{
var characterBuffer = new CharacterBufferRange(_textRun.CharacterBufferReference, _textRun.Length);
fixed (char* charsPtr = characterBuffer.Span)
{
return new string(charsPtr, 0, _textRun.Length);
}
}
}
}
public string Text => _textRun.Text.ToString();
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"/>
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)
{
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.Runtime.CompilerServices;
using System.Threading;
using Avalonia.Collections.Pooled;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
@ -27,7 +28,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// as much as possible.
/// </para>
/// </remarks>
internal sealed class BidiAlgorithm
internal struct BidiAlgorithm : IDisposable
{
/// <summary>
/// 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 reverse mapping maps the end index to the start index.
/// </remarks>
private readonly BidiDictionary<int, int> _isolatePairs = new BidiDictionary<int, int>();
private BidiDictionary<int, int>? _isolatePairs;
/// <summary>
/// The working BiDi classes
@ -118,7 +119,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary>
/// A stack of pending isolate openings used by FindIsolatePairs()
/// </summary>
private readonly Stack<int> _pendingIsolateOpenings = new Stack<int>();
private Stack<int>? _pendingIsolateOpenings;
/// <summary>
/// The level of the isolating run currently being processed
@ -184,7 +185,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary>
/// Initializes a new instance of the <see cref="BidiAlgorithm"/> class.
/// </summary>
internal BidiAlgorithm()
public BidiAlgorithm()
{
}
@ -227,7 +228,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
ArraySlice<sbyte>? outLevels)
{
// Reset state
_isolatePairs.Clear();
_isolatePairs?.Clear();
_workingClassesBuffer.Clear();
_levelRuns.Clear();
_resolvedLevelsBuffer.Clear();
@ -323,7 +324,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
// Skip isolate pairs
// (Because we're working with a slice, we need to adjust the indices
// 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;
}
@ -358,7 +359,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_hasIsolates = false;
// BD9...
_pendingIsolateOpenings.Clear();
_pendingIsolateOpenings?.Clear();
for (var i = 0; i < _originalClasses.Length; i++)
{
@ -370,14 +371,16 @@ namespace Avalonia.Media.TextFormatting.Unicode
case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate:
{
_pendingIsolateOpenings ??= new Stack<int>();
_pendingIsolateOpenings.Push(i);
_hasIsolates = true;
break;
}
case BidiClass.PopDirectionalIsolate:
{
if (_pendingIsolateOpenings.Count > 0)
if (_pendingIsolateOpenings?.Count > 0)
{
_isolatePairs ??= new BidiDictionary<int, int>();
_isolatePairs.Add(_pendingIsolateOpenings.Pop(), i);
}
@ -498,7 +501,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
if (resolvedIsolate == BidiClass.FirstStrongIsolate)
{
if (!_isolatePairs.TryGetValue(i, out var endOfIsolate))
if (_isolatePairs == null || !_isolatePairs.TryGetValue(i, out var endOfIsolate))
{
endOfIsolate = _originalClasses.Length;
}
@ -829,7 +832,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1];
var lastType = _originalClasses[lastCharacterIndex];
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
runIndex = FindRunForIndex(nextRunIndex);
@ -1714,5 +1717,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
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
/// for each character required for the bidirectional Unicode algorithm
/// </summary>
internal class BidiData
internal struct BidiData : IDisposable
{
private ArrayBuilder<BidiClass> _classes;
private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes;
@ -64,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Appends text to the bidi data.
/// </summary>
/// <param name="text">The text to process.</param>
public void Append(CharacterBufferRange text)
public void Append(ReadOnlySpan<char> text)
{
_classes.Add(text.Length);
_pairedBracketTypes.Add(text.Length);
@ -181,5 +181,15 @@ namespace Avalonia.Media.TextFormatting.Unicode
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="count">The count of character that were read.</param>
/// <returns></returns>
public static Codepoint ReadAt(IReadOnlyList<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)
public static Codepoint ReadAt(ReadOnlySpan<char> text, int index, out int count)
{
count = 1;

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

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

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

@ -7,11 +7,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary>
public readonly ref struct Grapheme
{
public Grapheme(Codepoint firstCodepoint, int offset, int length)
public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
{
FirstCodepoint = firstCodepoint;
Offset = offset;
Length = length;
Text = text;
}
/// <summary>
@ -20,13 +19,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
public Codepoint FirstCodepoint { get; }
/// <summary>
/// The Offset to the FirstCodepoint
/// The text of the grapheme cluster
/// </summary>
public int Offset { get; }
/// <summary>
/// The length of the grapheme cluster
/// </summary>
public int Length { get; }
public ReadOnlySpan<char> Text { 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.
using System.Collections.Generic;
using System;
using System.Runtime.InteropServices;
namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct GraphemeEnumerator
{
private CharacterBufferRange _text;
private ReadOnlySpan<char> _text;
public GraphemeEnumerator(CharacterBufferRange text)
public GraphemeEnumerator(ReadOnlySpan<char> text)
{
_text = text;
Current = default;
@ -185,9 +185,9 @@ namespace Avalonia.Media.TextFormatting.Unicode
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
}
@ -195,10 +195,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
[StructLayout(LayoutKind.Auto)]
private ref struct Processor
{
private readonly CharacterBufferRange _buffer;
private readonly ReadOnlySpan<char> _buffer;
private int _codeUnitLengthOfCurrentScalar;
internal Processor(CharacterBufferRange buffer)
internal Processor(ReadOnlySpan<char> buffer)
{
_buffer = buffer;
_codeUnitLengthOfCurrentScalar = 0;

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

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

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

@ -1,4 +1,4 @@
using Avalonia.Utilities;
using System;
namespace Avalonia.Media.TextFormatting
{
@ -7,52 +7,20 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public sealed class UnshapedTextRun : TextRun
{
public UnshapedTextRun(CharacterBufferReference characterBufferReference, int length,
TextRunProperties properties, sbyte biDiLevel)
public UnshapedTextRun(ReadOnlyMemory<char> text, TextRunProperties properties, sbyte biDiLevel)
{
CharacterBufferReference = characterBufferReference;
Length = length;
Text = text;
Properties = properties;
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 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 Avalonia.Input;
using Avalonia.Media;
namespace Avalonia.Platform
{
/// <summary>
/// A default implementation of <see cref="IPlatformSettings"/> for platforms which don't have
/// an OS-specific implementation.
/// A default implementation of <see cref="IPlatformSettings"/> for platforms.
/// </summary>
public class DefaultPlatformSettings : IPlatformSettings
{
public Size GetTapSize(PointerType type)
public virtual Size GetTapSize(PointerType type)
{
return type switch
{
@ -17,7 +17,7 @@ namespace Avalonia.Platform
_ => new(4, 4),
};
}
public Size GetDoubleTapSize(PointerType type)
public virtual Size GetDoubleTapSize(PointerType type)
{
return type switch
{
@ -25,8 +25,23 @@ namespace Avalonia.Platform
_ => 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.Collections.Generic;
using Avalonia.Metadata;
namespace Avalonia.Platform
@ -7,5 +8,8 @@ namespace Avalonia.Platform
/// Actual implementation of a glyph run that stores platform dependent resources.
/// </summary>
[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>
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;
namespace Avalonia.Platform
@ -13,9 +14,8 @@ namespace Avalonia.Platform
/// Shapes the specified region within the text and returns a shaped buffer.
/// </summary>
/// <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>
/// <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);
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;
AvaloniaResources = index.ToDictionary(r => GetPathRooted(r), r => (IAssetDescriptor)
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/
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
namespace Avalonia.Utilities
@ -11,7 +12,7 @@ namespace Avalonia.Utilities
/// A helper type for avoiding allocations while building arrays.
/// </summary>
/// <typeparam name="T">The type of item contained in the array.</typeparam>
internal struct ArrayBuilder<T>
internal struct ArrayBuilder<T> : IDisposable
where T : struct
{
private const int DefaultCapacity = 4;
@ -135,7 +136,7 @@ namespace Avalonia.Utilities
}
// Same expansion algorithm as List<T>.
var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u;
var newCapacity = length == 0 ? DefaultCapacity : length * 2;
if (newCapacity > MaxCoreClrArrayLength)
{
@ -144,14 +145,15 @@ namespace Avalonia.Utilities
if (newCapacity < min)
{
newCapacity = (uint)min;
newCapacity = min;
}
var array = new T[newCapacity];
var array = ArrayPool<T>.Shared.Rent(newCapacity);
if (_size > 0)
{
Array.Copy(_data!, array, _size);
ArrayPool<T>.Shared.Return(_data!);
}
_data = array;
@ -180,5 +182,13 @@ namespace Avalonia.Utilities
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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/
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@ -185,5 +186,13 @@ namespace Avalonia.Utilities
/// <inheritdoc/>
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.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Xml.Linq;
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace Avalonia.Utilities
{
#if !BUILDTASK
#if !BUILDTASK
public
#endif
#endif
static class AvaloniaResourcesIndexReaderWriter
{
private const int LastKnownVersion = 1;
public static List<AvaloniaResourcesIndexEntry> Read(Stream stream)
private const int XmlLegacyVersion = 1;
private const int BinaryCurrentVersion = 2;
public static List<AvaloniaResourcesIndexEntry> ReadIndex(Stream stream)
{
var ver = new BinaryReader(stream).ReadInt32();
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();
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
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")]
public static void Write(Stream stream, List<AvaloniaResourcesIndexEntry> entries)
private static List<AvaloniaResourcesIndexEntry> ReadXmlIndex()
=> 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);
new DataContractSerializer(typeof(AvaloniaResourcesIndex)).WriteObject(stream,
new AvaloniaResourcesIndex()
{
Entries = entries
var entryCount = reader.ReadInt32();
var entries = new List<AvaloniaResourcesIndexEntry>(entryCount);
for (var i = 0; i < entryCount; ++i)
{
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 byte[] Create(Dictionary<string, byte[]> data)
public static void WriteIndex(Stream output, List<AvaloniaResourcesIndexEntry> entries)
{
var sources = data.ToList();
var offsets = new Dictionary<string, int>();
var coffset = 0;
foreach (var s in sources)
using var writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true);
WriteIndex(writer, entries);
}
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;
coffset += s.Value.Length;
writer.Write(entry.Path ?? string.Empty);
writer.Write(entry.Offset);
writer.Write(entry.Size);
}
var index = sources.Select(s => new AvaloniaResourcesIndexEntry
{
Path = s.Key,
Size = s.Value.Length,
Offset = offsets[s.Key]
}).ToList();
var output = new MemoryStream();
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)
}
public static void WriteResources(Stream output, List<(string Path, int Size, Func<Stream> Open)> resources)
{
var entries = new List<AvaloniaResourcesIndexEntry>(resources.Count);
var offset = 0;
foreach (var resource in resources)
{
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]
#if !BUILDTASK
public
#endif
class AvaloniaResourcesIndex
{
[DataMember]
public List<AvaloniaResourcesIndexEntry> Entries { get; set; } = new List<AvaloniaResourcesIndexEntry>();
var posBeforeEntries = output.Position;
WriteIndex(writer, entries);
var posAfterEntries = output.Position;
var indexSize = (int) (posAfterEntries - posBeforeEntries);
output.Position = 0L;
writer.Write(indexSize);
output.Position = posAfterEntries;
foreach (var resource in resources)
{
using var resourceStream = resource.Open();
resourceStream.CopyTo(output);
}
}
}
[DataContract]
#if !BUILDTASK
public
#endif
class AvaloniaResourcesIndexEntry
{
[DataMember]
public string? Path { get; set; }
[DataMember]
public int Offset { get; set; }
[DataMember]
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">
<Link>Shared/AvaloniaResourcesIndex.cs</Link>
</Compile>
<Compile Include="../Avalonia.Base/Platform/Internal/Constants.cs">
<Link>Shared/Constants.cs</Link>
</Compile>
<Compile Include="../Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaResourceXamlInfo.cs">
<Link>Shared/AvaloniaResourceXamlInfo.cs</Link>
</Compile>

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

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
@ -10,9 +11,10 @@ namespace Avalonia.Build.Tasks
public bool Execute()
{
Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance);
var writtenFilePaths = new List<string>();
OutputPath = OutputPath ?? AssemblyFile;
RefOutputPath = RefOutputPath ?? RefAssemblyFile;
OutputPath ??= AssemblyFile;
RefOutputPath ??= RefAssemblyFile;
var outputPdb = GetPdbPath(OutputPath);
var input = AssemblyFile;
var refInput = RefOutputPath;
@ -21,8 +23,9 @@ namespace Avalonia.Build.Tasks
if (OriginalCopyPath != null)
{
var originalCopyPathRef = Path.ChangeExtension(OriginalCopyPath, ".ref.dll");
File.Copy(AssemblyFile, OriginalCopyPath, true);
writtenFilePaths.Add(OriginalCopyPath);
input = OriginalCopyPath;
File.Delete(AssemblyFile);
@ -30,6 +33,7 @@ namespace Avalonia.Build.Tasks
{
var copyPdb = GetPdbPath(OriginalCopyPath);
File.Copy(inputPdb, copyPdb, true);
writtenFilePaths.Add(copyPdb);
File.Delete(inputPdb);
inputPdb = copyPdb;
}
@ -39,6 +43,7 @@ namespace Avalonia.Build.Tasks
// 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.
File.Copy(RefAssemblyFile, originalCopyPathRef, true);
writtenFilePaths.Add(originalCopyPathRef);
refInput = originalCopyPathRef;
}
}
@ -53,13 +58,25 @@ namespace Avalonia.Build.Tasks
ProjectDirectory, VerifyIl, DefaultCompileBindings, outputImportance,
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, SkipXamlCompilation, DebuggerLaunch);
if (!res.Success)
{
WrittenFilePaths = writtenFilePaths.ToArray();
return false;
}
if (!res.WrittenFile)
{
File.Copy(input, OutputPath, true);
if(File.Exists(inputPdb))
if (File.Exists(inputPdb))
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;
}
@ -103,5 +120,8 @@ namespace Avalonia.Build.Tasks
public ITaskHost HostObject { 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)
{
var offsets = new Dictionary<Source, int>();
var coffset = 0;
foreach (var s in sources)
{
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);
}
AvaloniaResourcesIndexReaderWriter.WriteResources(
output,
sources.Select(source => (source.Path, source.Size, (Func<Stream>) source.Open)).ToList());
}
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.IO;
using System.Linq;
using Avalonia.Platform.Internal;
using Avalonia.Utilities;
using Mono.Cecil;
using Mono.Cecil.Cil;
@ -34,13 +36,13 @@ namespace Avalonia.Build.Tasks
{
_asm = asm;
_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)
return;
using (var stream = _embedded.GetResourceStream())
{
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;
foreach (var e in index)
{
@ -61,9 +63,18 @@ namespace Avalonia.Build.Tasks
if (_resources.Count == 0)
return;
_embedded = new EmbeddedResource("!AvaloniaResources", ManifestResourceAttributes.Public,
AvaloniaResourcesIndexReaderWriter.Create(_resources.ToDictionary(x => x.Key,
x => x.Value.FileContents)));
var output = new MemoryStream();
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);
}

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

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

2
src/Avalonia.Controls/ComboBox.cs

@ -361,7 +361,7 @@ namespace Avalonia.Controls
_subscriptionsOnOpen.Clear();
var toplevel = this.GetVisualRoot() as TopLevel;
var toplevel = TopLevel.GetTopLevel(this);
if (toplevel != null)
{
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 void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { }
/// <inheritdoc/>
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);

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

@ -163,6 +163,12 @@ namespace Avalonia.Platform
/// </summary>
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>
/// Gets the <see cref="AcrylicPlatformCompensationLevels"/> for the platform.
/// </summary>

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

@ -428,6 +428,8 @@ namespace Avalonia.Controls.Presenters
Offset = newOffset;
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()
{
_textLayout?.Dispose();
_textLayout = null;
InvalidateMeasure();
@ -597,6 +598,7 @@ namespace Avalonia.Controls.Presenters
{
_constraint = availableSize;
_textLayout?.Dispose();
_textLayout = null;
InvalidateArrange();
@ -622,6 +624,7 @@ namespace Avalonia.Controls.Presenters
_constraint = new Size(Math.Ceiling(finalSize.Width), double.PositiveInfinity);
_textLayout?.Dispose();
_textLayout = null;
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)
{
var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup();
var platform = TopLevel.GetTopLevel(target)?.PlatformImpl?.CreatePopup();
if (platform != null)
return new PopupRoot((TopLevel)target.GetVisualRoot()!, platform, dependencyResolver);

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

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

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

@ -32,26 +32,42 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="Checked"/> event.
/// </summary>
[Obsolete("Use IsCheckedChangedEvent instead.")]
public static readonly RoutedEvent<RoutedEventArgs> CheckedEvent =
RoutedEvent.Register<ToggleButton, RoutedEventArgs>(nameof(Checked), RoutingStrategies.Bubble);
RoutedEvent.Register<ToggleButton, RoutedEventArgs>(
nameof(Checked),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="Unchecked"/> event.
/// </summary>
[Obsolete("Use IsCheckedChangedEvent instead.")]
public static readonly RoutedEvent<RoutedEventArgs> UncheckedEvent =
RoutedEvent.Register<ToggleButton, RoutedEventArgs>(nameof(Unchecked), RoutingStrategies.Bubble);
RoutedEvent.Register<ToggleButton, RoutedEventArgs>(
nameof(Unchecked),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="Unchecked"/> event.
/// </summary>
[Obsolete("Use IsCheckedChangedEvent instead.")]
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;
static ToggleButton()
{
IsCheckedProperty.Changed.AddClassHandler<ToggleButton>((x, e) => x.OnIsCheckedChanged(e));
}
public ToggleButton()
@ -62,6 +78,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Raised when a <see cref="ToggleButton"/> is checked.
/// </summary>
[Obsolete("Use IsCheckedChanged instead.")]
public event EventHandler<RoutedEventArgs>? Checked
{
add => AddHandler(CheckedEvent, value);
@ -71,6 +88,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Raised when a <see cref="ToggleButton"/> is unchecked.
/// </summary>
[Obsolete("Use IsCheckedChanged instead.")]
public event EventHandler<RoutedEventArgs>? Unchecked
{
add => AddHandler(UncheckedEvent, value);
@ -80,12 +98,22 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Raised when a <see cref="ToggleButton"/> is neither checked nor unchecked.
/// </summary>
[Obsolete("Use IsCheckedChanged instead.")]
public event EventHandler<RoutedEventArgs>? Indeterminate
{
add => AddHandler(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>
/// Gets or sets whether the <see cref="ToggleButton"/> is checked.
/// </summary>
@ -147,6 +175,7 @@ namespace Avalonia.Controls.Primitives
/// Called when <see cref="IsChecked"/> becomes true.
/// </summary>
/// <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)
{
RaiseEvent(e);
@ -156,6 +185,7 @@ namespace Avalonia.Controls.Primitives
/// Called when <see cref="IsChecked"/> becomes false.
/// </summary>
/// <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)
{
RaiseEvent(e);
@ -165,31 +195,51 @@ namespace Avalonia.Controls.Primitives
/// Called when <see cref="IsChecked"/> becomes null.
/// </summary>
/// <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)
{
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()
{
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:
OnChecked(new RoutedEventArgs(CheckedEvent));
break;
case false:
OnUnchecked(new RoutedEventArgs(UncheckedEvent));
break;
default:
OnIndeterminate(new RoutedEventArgs(IndeterminateEvent));
break;
var newValue = change.GetNewValue<bool?>();
#pragma warning disable CS0618 // Type or member is obsolete
switch (newValue)
{
case true:
OnChecked(new RoutedEventArgs(CheckedEvent));
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>
/// Raised when the user presses the primary part of the <see cref="SplitButton"/>.
/// </summary>
public event EventHandler<RoutedEventArgs> Click
public event EventHandler<RoutedEventArgs>? Click
{
add => AddHandler(ClickEvent, value);
remove => RemoveHandler(ClickEvent, value);
@ -35,7 +35,9 @@ namespace Avalonia.Controls
/// Defines the <see cref="Click"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> ClickEvent =
RoutedEvent.Register<SplitButton, RoutedEventArgs>(nameof(Click), RoutingStrategies.Bubble);
RoutedEvent.Register<SplitButton, RoutedEventArgs>(
nameof(Click),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="Command"/> property.
@ -69,10 +71,6 @@ namespace Avalonia.Controls
private IDisposable? _flyoutPropertyChangedDisposable;
////////////////////////////////////////////////////////////////////////
// Constructor / Destructors
////////////////////////////////////////////////////////////////////////
/// <summary>
/// Initializes a new instance of the <see cref="SplitButton"/> class.
/// </summary>
@ -80,10 +78,6 @@ namespace Avalonia.Controls
{
}
////////////////////////////////////////////////////////////////////////
// Properties
////////////////////////////////////////////////////////////////////////
/// <summary>
/// Gets or sets the <see cref="ICommand"/> invoked when the primary part is pressed.
/// </summary>
@ -124,10 +118,6 @@ namespace Avalonia.Controls
/// <inheritdoc/>
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
////////////////////////////////////////////////////////////////////////
// Methods
////////////////////////////////////////////////////////////////////////
/// <inheritdoc/>
void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e);
@ -222,10 +212,6 @@ namespace Avalonia.Controls
}
}
////////////////////////////////////////////////////////////////////////
// OnEvent Overridable Methods
////////////////////////////////////////////////////////////////////////
/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
@ -429,10 +415,6 @@ namespace Avalonia.Controls
// Available for derived types
}
////////////////////////////////////////////////////////////////////////
// Event Handling
////////////////////////////////////////////////////////////////////////
/// <summary>
/// Event handler for when the internal primary button part is pressed.
/// </summary>

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

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

26
src/Avalonia.Controls/TextBlock.cs

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

6
src/Avalonia.Controls/TextBox.cs

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

6
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

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

72
src/Avalonia.Controls/TopLevel.cs

@ -7,6 +7,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.LogicalTree;
@ -17,6 +18,8 @@ using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using Avalonia.Input.Platform;
using System.Linq;
namespace Avalonia.Controls
{
@ -76,6 +79,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty =
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>
ResourcesChangedWeakEvent = WeakEvent.Register<IResourceHost, ResourcesChangedEventArgs>(
(s, h) => s.ResourcesChanged += h,
@ -89,6 +98,7 @@ namespace Avalonia.Controls
private readonly IGlobalStyles? _globalStyles;
private readonly PointerOverPreProcessor? _pointerOverPreProcessor;
private readonly IDisposable? _pointerOverPreProcessorSubscription;
private readonly IDisposable? _backGestureSubscription;
private Size _clientSize;
private Size? _frameSize;
private WindowTransparencyLevel _actualTransparencyLevel;
@ -205,6 +215,48 @@ namespace Avalonia.Controls
_pointerOverPreProcessor = new PointerOverPreProcessor(this);
_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>
@ -263,6 +315,15 @@ namespace Avalonia.Controls
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
{
get
@ -341,6 +402,16 @@ namespace Avalonia.Controls
{
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>
/// Creates the layout manager for this <see cref="TopLevel" />.
@ -372,6 +443,7 @@ namespace Avalonia.Controls
_pointerOverPreProcessor?.OnCompleted();
_pointerOverPreProcessorSubscription?.Dispose();
_backGestureSubscription?.Dispose();
PlatformImpl = null;

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

@ -182,7 +182,9 @@ namespace Avalonia.DesignerSupport.Remote
public bool IsClientAreaExtendedToDecorations { get; }
public bool NeedsManagedDecorations => false;
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { }
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);
}
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/"

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.IO;
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Avalonia.Controls.Platform;
using Avalonia.Reactive;
using Avalonia.Reactive;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Threading;
namespace Avalonia.FreeDesktop

5
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

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

6
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -145,15 +145,13 @@ namespace Avalonia.Headless
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 fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
var characterBufferRange = new CharacterBufferRange(text, length);
return new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel);
return new ShapedBuffer(text, text.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.Interactivity;
using Avalonia.Native.Interop;
using Avalonia.VisualTree;
namespace Avalonia.Native
{
@ -26,7 +25,7 @@ namespace Avalonia.Native
if (element == null)
return null;
var visual = (Visual)element;
return visual.GetVisualRoot() as TopLevel;
return TopLevel.GetTopLevel(visual);
}
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<IPlatformIconLoader>().ToSingleton<IconLoader>()
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IPlatformSettings>().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings()))
.Bind<IWindowingPlatform>().ToConstant(this)
.Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))
.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.Linq;
using Avalonia.Reactive;
using Avalonia.Reactive;
using Avalonia.Controls.Platform;
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 void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
_native.SetFrameThemeVariant((AvnPlatformThemeVariant)themeVariant);
}
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0, 0);
public IPlatformHandle Handle { get; private set; }

18
src/Avalonia.Native/avn.idl

@ -473,6 +473,14 @@ enum AvnWindowTransparencyMode
Blur
}
enum AvnPlatformThemeVariant
{
Light,
Dark,
HighContrastLight,
HighContrastDark,
}
[uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]
interface IAvaloniaNativeFactory : IUnknown
{
@ -494,6 +502,7 @@ interface IAvaloniaNativeFactory : IUnknown
HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv);
HRESULT CreateTrayIcon(IAvnTrayIcon** ppv);
HRESULT CreateApplicationCommands(IAvnApplicationCommands** ppv);
HRESULT CreatePlatformSettings(IAvnPlatformSettings** ppv);
}
[uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)]
@ -535,6 +544,7 @@ interface IAvnWindowBase : IUnknown
HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard* clipboard, IAvnDndResultCallback* cb, [intptr]void* sourceHandle);
HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode);
HRESULT SetFrameThemeVariant(AvnPlatformThemeVariant mode);
}
[uuid(83e588f3-6981-4e48-9ea0-e1e569f79a91), cpp-virtual-inherits]
@ -906,3 +916,11 @@ interface IAvnAutomationNode : IUnknown
void PropertyChanged(AvnAutomationProperty property);
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<ICursorFactory>().ToConstant(new X11CursorFactory(Display))
.Bind<IClipboard>().ToConstant(new X11Clipboard(this))
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IPlatformSettings>().ToSingleton<DBusPlatformSettings>()
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this));

2
src/Avalonia.X11/X11Window.cs

@ -1226,6 +1226,8 @@ namespace Avalonia.X11
public WindowTransparencyLevel TransparencyLevel =>
_transparencyHelper?.CurrentLevel ?? WindowTransparencyLevel.None;
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8);
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
{
internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider,
ITopLevelWithSystemNavigationManager
{
private Size _clientSize;
private IInputRoot? _inputRoot;
@ -228,11 +229,18 @@ namespace Avalonia.Browser
public IKeyboardDevice KeyboardDevice { get; } = BrowserWindowingPlatform.Keyboard;
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 ITextInputMethodImpl TextInputMethod => _avaloniaView;
public INativeControlHostImpl? NativeControlHost { get; }
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(
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>]
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