Browse Source

Merge branch 'button-memory-leak' into cleanup-platformsupport

pull/8183/head
Max Katz 4 years ago
parent
commit
0488d0ddda
  1. 9
      Avalonia.sln
  2. 6
      build/SkiaSharp.props
  3. 2
      native/Avalonia.Native/src/OSX/AvnPanelWindow.mm
  4. 4
      native/Avalonia.Native/src/OSX/AvnView.mm
  5. 81
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  6. 2
      native/Avalonia.Native/src/OSX/INSWindowHolder.h
  7. 6
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  8. 19
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  9. 8
      native/Avalonia.Native/src/OSX/WindowImpl.h
  10. 117
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  11. 1
      native/Avalonia.Native/src/OSX/WindowProtocol.h
  12. 15
      readme.md
  13. 35
      samples/ControlCatalog.Android/EmbedSample.Android.cs
  14. 6
      samples/ControlCatalog.Android/MainActivity.cs
  15. 14
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  16. 35
      samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs
  17. 58
      samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs
  18. 0
      samples/ControlCatalog.NetCore/NativeControls/Gtk/nodes-license.md
  19. 0
      samples/ControlCatalog.NetCore/NativeControls/Gtk/nodes.mp4
  20. 29
      samples/ControlCatalog.NetCore/NativeControls/Mac/EmbedSample.Mac.cs
  21. 38
      samples/ControlCatalog.NetCore/NativeControls/Mac/MacHelper.cs
  22. 45
      samples/ControlCatalog.NetCore/NativeControls/Win/EmbedSample.Win.cs
  23. 73
      samples/ControlCatalog.NetCore/NativeControls/Win/WinApi.cs
  24. 8
      samples/ControlCatalog.NetCore/Program.cs
  25. 28
      samples/ControlCatalog.NetCore/app.manifest
  26. 4
      samples/ControlCatalog.Web/App.razor.cs
  27. 34
      samples/ControlCatalog.Web/EmbedSample.Browser.cs
  28. 70
      samples/ControlCatalog.Web/Shared/MainLayout.razor.css
  29. 44
      samples/ControlCatalog.Web/wwwroot/css/app.css
  30. 11
      samples/ControlCatalog.Web/wwwroot/js/app.js
  31. 9
      samples/ControlCatalog.iOS/AppDelegate.cs
  32. 38
      samples/ControlCatalog.iOS/EmbedSample.iOS.cs
  33. 15
      samples/ControlCatalog/ControlCatalog.csproj
  34. 7
      samples/ControlCatalog/MainView.xaml
  35. 2
      samples/ControlCatalog/Pages/DialogsPage.xaml
  36. 13
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  37. 68
      samples/ControlCatalog/Pages/NativeEmbedPage.xaml
  38. 84
      samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs
  39. 8
      samples/interop/NativeEmbedSample/App.xaml
  40. 22
      samples/interop/NativeEmbedSample/App.xaml.cs
  41. 121
      samples/interop/NativeEmbedSample/EmbedSample.cs
  42. 58
      samples/interop/NativeEmbedSample/GtkHelper.cs
  43. 39
      samples/interop/NativeEmbedSample/MacHelper.cs
  44. 52
      samples/interop/NativeEmbedSample/MainWindow.xaml
  45. 36
      samples/interop/NativeEmbedSample/MainWindow.xaml.cs
  46. 31
      samples/interop/NativeEmbedSample/NativeEmbedSample.csproj
  47. 17
      samples/interop/NativeEmbedSample/Program.cs
  48. 74
      samples/interop/NativeEmbedSample/WinApi.cs
  49. 32
      src/Android/Avalonia.Android/AndroidViewControlHandle.cs
  50. 7
      src/Android/Avalonia.Android/AvaloniaView.cs
  51. 139
      src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs
  52. 10
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  53. 9
      src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
  54. 11
      src/Avalonia.Base/Media/TextFormatting/TextBounds.cs
  55. 29
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  56. 376
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  57. 39
      src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs
  58. 4
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  59. 4
      src/Avalonia.Base/Styling/Style.cs
  60. 5
      src/Avalonia.Controls/ContextMenu.cs
  61. 5
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  62. 5
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  63. 6
      src/Avalonia.Controls/TextBlock.cs
  64. 2
      src/Avalonia.Controls/Viewbox.cs
  65. 4
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  66. 4
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  67. 2
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  68. 32
      src/Avalonia.FreeDesktop/DBusFileChooser.cs
  69. 24
      src/Avalonia.FreeDesktop/DBusHelper.cs
  70. 16
      src/Avalonia.FreeDesktop/DBusRequest.cs
  71. 102
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  72. 2
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  73. 74
      src/Avalonia.Themes.Fluent/Controls/Button.xaml
  74. 464
      src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
  75. 10
      src/Avalonia.X11/X11Platform.cs
  76. 2
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  77. 57
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
  78. 146
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  79. 34
      src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs
  80. 152
      src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs
  81. 56
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts
  82. 35
      src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs
  83. 23
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  84. 15
      src/Web/Avalonia.Web.Blazor/WinStubs.cs
  85. 2
      src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs
  86. 2
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  87. 4
      src/Windows/Avalonia.Win32/Win32NativeControlHost.cs
  88. 4
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  89. 160
      src/iOS/Avalonia.iOS/NativeControlHostImpl.cs
  90. 21
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  91. 47
      tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
  92. 26
      tests/Avalonia.Controls.UnitTests/ViewboxTests.cs
  93. 32
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_Method.cs
  94. 8
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  95. 184
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  96. 2
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  97. 2
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs

9
Avalonia.sln

@ -97,6 +97,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\DevAnalyzers.props = build\DevAnalyzers.props
build\EmbedXaml.props = build\EmbedXaml.props
build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
build\ImageSharp.props = build\ImageSharp.props
build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props
build\Microsoft.CSharp.props = build\Microsoft.CSharp.props
@ -117,7 +118,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\System.Memory.props = build\System.Memory.props
build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
build\XUnit.props = build\XUnit.props
build\ImageSharp.props = build\ImageSharp.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}"
@ -179,8 +179,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeEmbedSample", "samples\interop\NativeEmbedSample\NativeEmbedSample.csproj", "{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}"
@ -419,6 +417,10 @@ Global
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.Build.0 = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.Build.0 = Release|Any CPU
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -552,7 +554,6 @@ Global
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
{909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098}

6
build/SkiaSharp.props

@ -1,7 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.88.0" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.0" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.0" />
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.1" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.1-preview.1" />
</ItemGroup>
</Project>

2
native/Avalonia.Native/src/OSX/AvnPanelWindow.mm

@ -3,8 +3,6 @@
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#pragma once
#define IS_NSPANEL
#include "AvnWindow.mm"

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

@ -222,7 +222,7 @@
- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type
{
bool triggerInputWhenDisabled = type != Move;
bool triggerInputWhenDisabled = type != Move && type != LeaveWindow;
if([self ignoreUserInput: triggerInputWhenDisabled])
{
@ -709,4 +709,4 @@
return [[self accessibilityChild] accessibilityFocusedUIElement];
}
@end
@end

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

@ -68,7 +68,7 @@
}
}
- (void)performClose:(id)sender
- (void)performClose:(id _Nullable )sender
{
if([[self delegate] respondsToSelector:@selector(windowShouldClose:)])
{
@ -147,7 +147,7 @@
}
}
-(void) applyMenu:(AvnMenu *)menu
-(void) applyMenu:(AvnMenu *_Nullable)menu
{
if(menu == nullptr)
{
@ -157,7 +157,7 @@
_menu = menu;
}
-(CLASS_NAME*) initWithParent: (WindowBaseImpl*) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask;
-(CLASS_NAME*_Nonnull) initWithParent: (WindowBaseImpl*_Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask;
{
// https://jameshfisher.com/2020/07/10/why-is-the-contentrect-of-my-nswindow-ignored/
// create nswindow with specific contentRect, otherwise we wont be able to resize the window
@ -176,14 +176,15 @@
_isExtended = false;
#ifdef IS_NSPANEL
[self setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces|NSWindowCollectionBehaviorFullScreenAuxiliary];
#endif
if(self.isDialog)
{
[self setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces|NSWindowCollectionBehaviorFullScreenAuxiliary];
}
return self;
}
- (BOOL)windowShouldClose:(NSWindow *)sender
- (BOOL)windowShouldClose:(NSWindow *_Nonnull)sender
{
auto window = dynamic_cast<WindowImpl*>(_parent.getRaw());
@ -195,21 +196,28 @@
return true;
}
- (void)windowDidChangeBackingProperties:(NSNotification *)notification
- (void)windowDidChangeBackingProperties:(NSNotification *_Nonnull)notification
{
[self backingScaleFactor];
}
- (void)windowWillClose:(NSNotification *)notification
- (void)windowWillClose:(NSNotification *_Nonnull)notification
{
_closed = true;
if(_parent)
{
ComPtr<WindowBaseImpl> parent = _parent;
_parent = NULL;
[self restoreParentWindow];
auto window = dynamic_cast<WindowImpl*>(parent.getRaw());
if(window != nullptr)
{
window->SetParent(nullptr);
}
parent->BaseEvents->Closed();
[parent->View onClosed];
}
@ -220,17 +228,11 @@
if(_canBecomeKeyWindow)
{
// If the window has a child window being shown as a dialog then don't allow it to become the key window.
for(NSWindow* uch in [self childWindows])
auto parent = dynamic_cast<WindowImpl*>(_parent.getRaw());
if(parent != nullptr)
{
if (![uch conformsToProtocol:@protocol(AvnWindowProtocol)])
{
continue;
}
id <AvnWindowProtocol> ch = (id <AvnWindowProtocol>) uch;
if(ch.isDialog)
return false;
return parent->CanBecomeKeyWindow();
}
return true;
@ -259,6 +261,10 @@
-(void) setEnabled:(bool)enable
{
_isEnabled = enable;
[[self standardWindowButton:NSWindowCloseButton] setEnabled:enable];
[[self standardWindowButton:NSWindowMiniaturizeButton] setEnabled:enable];
[[self standardWindowButton:NSWindowZoomButton] setEnabled:enable];
}
-(void)becomeKeyWindow
@ -273,17 +279,12 @@
[super becomeKeyWindow];
}
-(void) restoreParentWindow;
- (void)windowDidBecomeKey:(NSNotification *_Nonnull)notification
{
auto parent = [self parentWindow];
if(parent != nil)
{
[parent removeChildWindow:self];
}
_parent->BringToFront();
}
- (void)windowDidMiniaturize:(NSNotification *)notification
- (void)windowDidMiniaturize:(NSNotification *_Nonnull)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
@ -293,7 +294,7 @@
}
}
- (void)windowDidDeminiaturize:(NSNotification *)notification
- (void)windowDidDeminiaturize:(NSNotification *_Nonnull)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
@ -303,7 +304,7 @@
}
}
- (void)windowDidResize:(NSNotification *)notification
- (void)windowDidResize:(NSNotification *_Nonnull)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
@ -313,7 +314,7 @@
}
}
- (void)windowWillExitFullScreen:(NSNotification *)notification
- (void)windowWillExitFullScreen:(NSNotification *_Nonnull)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
@ -323,7 +324,7 @@
}
}
- (void)windowDidExitFullScreen:(NSNotification *)notification
- (void)windowDidExitFullScreen:(NSNotification *_Nonnull)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
@ -346,7 +347,7 @@
}
}
- (void)windowWillEnterFullScreen:(NSNotification *)notification
- (void)windowWillEnterFullScreen:(NSNotification *_Nonnull)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
@ -356,7 +357,7 @@
}
}
- (void)windowDidEnterFullScreen:(NSNotification *)notification
- (void)windowDidEnterFullScreen:(NSNotification *_Nonnull)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
@ -367,7 +368,7 @@
}
}
- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame
- (BOOL)windowShouldZoom:(NSWindow *_Nonnull)window toFrame:(NSRect)newFrame
{
return true;
}
@ -378,11 +379,13 @@
_parent->BaseEvents->Deactivated();
[self showAppMenuOnly];
[self invalidateShadow];
[super resignKeyWindow];
}
- (void)windowDidMove:(NSNotification *)notification
- (void)windowDidMove:(NSNotification *_Nonnull)notification
{
AvnPoint position;
@ -414,7 +417,7 @@
return pt;
}
- (void)sendEvent:(NSEvent *)event
- (void)sendEvent:(NSEvent *_Nonnull)event
{
[super sendEvent:event];
@ -437,8 +440,10 @@
_parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, static_cast<uint32>([event timestamp] * 1000), AvnInputModifiersNone, point, delta);
}
_parent->BringToFront();
}
break;
break;
case NSEventTypeMouseEntered:
{

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

@ -11,7 +11,7 @@
struct INSWindowHolder
{
virtual NSWindow* _Nonnull GetNSWindow () = 0;
virtual NSView* _Nonnull GetNSView () = 0;
virtual AvnView* _Nonnull GetNSView () = 0;
};
#endif //AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H

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

@ -26,7 +26,7 @@ BEGIN_INTERFACE_MAP()
virtual ~WindowBaseImpl();
WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl);
WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, bool usePanel = false);
virtual HRESULT ObtainNSWindowHandle(void **ret) override;
@ -38,7 +38,7 @@ BEGIN_INTERFACE_MAP()
virtual NSWindow *GetNSWindow() override;
virtual NSView *GetNSView() override;
virtual AvnView *GetNSView() override;
virtual HRESULT Show(bool activate, bool isDialog) override;
@ -99,6 +99,8 @@ BEGIN_INTERFACE_MAP()
virtual bool IsDialog();
id<AvnWindowProtocol> GetWindowProtocol ();
virtual void BringToFront ();
protected:
virtual NSWindowStyleMask GetStyle();

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

@ -21,7 +21,7 @@ WindowBaseImpl::~WindowBaseImpl() {
Window = nullptr;
}
WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) {
WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, bool usePanel) {
_shown = false;
_inResize = false;
BaseEvents = events;
@ -36,8 +36,10 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl)
lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX};
lastMinSize = NSSize { 0, 0 };
Window = nullptr;
lastMenu = nullptr;
CreateNSWindow(usePanel);
InitialiseNSWindow();
}
HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) {
@ -68,7 +70,7 @@ NSWindow *WindowBaseImpl::GetNSWindow() {
return Window;
}
NSView *WindowBaseImpl::GetNSView() {
AvnView *WindowBaseImpl::GetNSView() {
return View;
}
@ -88,7 +90,6 @@ HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) {
START_COM_CALL;
@autoreleasepool {
CreateNSWindow(isDialog);
InitialiseNSWindow();
if(hasPosition)
@ -143,8 +144,6 @@ HRESULT WindowBaseImpl::Hide() {
@autoreleasepool {
if (Window != nullptr) {
[Window orderOut:Window];
[GetWindowProtocol() restoreParentWindow];
}
return S_OK;
@ -558,6 +557,8 @@ void WindowBaseImpl::CreateNSWindow(bool isDialog) {
CleanNSWindow();
Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()];
[Window setHidesOnDeactivate:false];
}
} else {
if (![Window isKindOfClass:[AvnWindow class]]) {
@ -585,6 +586,7 @@ void WindowBaseImpl::InitialiseNSWindow() {
[Window setOpaque:false];
[Window setHasShadow:true];
[Window invalidateShadow];
if (lastMenu != nullptr) {
@ -608,6 +610,11 @@ id <AvnWindowProtocol> WindowBaseImpl::GetWindowProtocol() {
return (id <AvnWindowProtocol>) Window;
}
void WindowBaseImpl::BringToFront()
{
// do nothing.
}
extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl)
{
@autoreleasepool

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

@ -8,10 +8,12 @@
#import "WindowBaseImpl.h"
#include "IWindowStateChanged.h"
#include <list>
class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged
{
private:
bool _isEnabled;
bool _canResize;
bool _fullScreenActive;
SystemDecorations _decorations;
@ -22,6 +24,8 @@ private:
bool _transitioningWindowState;
bool _isClientAreaExtended;
bool _isDialog;
WindowImpl* _parent;
std::list<WindowImpl*> _children;
AvnExtendClientAreaChromeHints _extendClientHints;
FORWARD_IUNKNOWN()
@ -90,6 +94,10 @@ BEGIN_INTERFACE_MAP()
virtual bool IsDialog() override;
virtual void OnInitialiseNSWindow() override;
virtual void BringToFront () override;
bool CanBecomeKeyWindow ();
protected:
virtual NSWindowStyleMask GetStyle() override;

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

@ -10,6 +10,8 @@
#include "WindowProtocol.h"
WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBaseImpl(events, gl) {
_isEnabled = true;
_children = std::list<WindowImpl*>();
_isClientAreaExtended = false;
_extendClientHints = AvnDefaultChrome;
_fullScreenActive = false;
@ -20,6 +22,7 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase
_lastWindowState = Normal;
_actualWindowState = Normal;
_lastTitle = @"";
_parent = nullptr;
WindowEvents = events;
}
@ -28,24 +31,12 @@ void WindowImpl::HideOrShowTrafficLights() {
return;
}
for (id subview in Window.contentView.superview.subviews) {
if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) {
NSView *titlebarView = [subview subviews][0];
for (id button in titlebarView.subviews) {
if ([button isKindOfClass:[NSButton class]]) {
if (_isClientAreaExtended) {
auto wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
[button setHidden:!wantsChrome];
} else {
[button setHidden:(_decorations != SystemDecorationsFull)];
}
[button setWantsLayer:true];
}
}
}
}
bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
bool hasTrafficLights = _isClientAreaExtended ? !wantsChrome : _decorations != SystemDecorationsFull;
[[Window standardWindowButton:NSWindowCloseButton] setHidden:hasTrafficLights];
[[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:hasTrafficLights];
[[Window standardWindowButton:NSWindowZoomButton] setHidden:hasTrafficLights];
}
void WindowImpl::OnInitialiseNSWindow(){
@ -61,6 +52,11 @@ void WindowImpl::OnInitialiseNSWindow(){
[GetWindowProtocol() setIsExtended:true];
SetExtendClientArea(true);
}
if(_parent != nullptr)
{
SetParent(_parent);
}
}
HRESULT WindowImpl::Show(bool activate, bool isDialog) {
@ -81,7 +77,9 @@ HRESULT WindowImpl::SetEnabled(bool enable) {
START_COM_CALL;
@autoreleasepool {
_isEnabled = enable;
[GetWindowProtocol() setEnabled:enable];
UpdateStyle();
return S_OK;
}
}
@ -90,26 +88,68 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) {
START_COM_CALL;
@autoreleasepool {
if (parent == nullptr)
return E_POINTER;
if(_parent != nullptr)
{
_parent->_children.remove(this);
_parent->BringToFront();
}
auto cparent = dynamic_cast<WindowImpl *>(parent);
if (cparent == nullptr)
return E_INVALIDARG;
// If one tries to show a child window with a minimized parent window, then the parent window will be
// restored but macOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive
// state. Detect this and explicitly restore the parent window ourselves to avoid this situation.
if (cparent->WindowState() == Minimized)
cparent->SetWindowState(Normal);
_parent = cparent;
if(_parent != nullptr && Window != nullptr){
// If one tries to show a child window with a minimized parent window, then the parent window will be
// restored but macOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive
// state. Detect this and explicitly restore the parent window ourselves to avoid this situation.
if (cparent->WindowState() == Minimized)
cparent->SetWindowState(Normal);
[Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary];
cparent->_children.push_back(this);
UpdateStyle();
}
[Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary];
[cparent->Window addChildWindow:Window ordered:NSWindowAbove];
return S_OK;
}
}
UpdateStyle();
void WindowImpl::BringToFront()
{
if(Window != nullptr)
{
if(IsDialog())
{
Activate();
}
else
{
[Window orderFront:nullptr];
}
[Window invalidateShadow];
for(auto iterator = _children.begin(); iterator != _children.end(); iterator++)
{
(*iterator)->BringToFront();
}
}
}
return S_OK;
bool WindowImpl::CanBecomeKeyWindow()
{
for(auto iterator = _children.begin(); iterator != _children.end(); iterator++)
{
if((*iterator)->IsDialog())
{
return false;
}
}
return true;
}
void WindowImpl::StartStateTransition() {
@ -523,7 +563,12 @@ bool WindowImpl::IsDialog() {
}
NSWindowStyleMask WindowImpl::GetStyle() {
unsigned long s = this->_isDialog ? NSWindowStyleMaskDocModalWindow : NSWindowStyleMaskBorderless;
unsigned long s = NSWindowStyleMaskBorderless;
if(_actualWindowState == FullScreen)
{
s |= NSWindowStyleMaskFullScreen;
}
switch (_decorations) {
case SystemDecorationsNone:
@ -535,15 +580,15 @@ NSWindowStyleMask WindowImpl::GetStyle() {
break;
case SystemDecorationsFull:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless;
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable;
if (_canResize) {
if (_canResize && _isEnabled) {
s = s | NSWindowStyleMaskResizable;
}
break;
}
if ([Window parentWindow] == nullptr) {
if (!IsDialog()) {
s |= NSWindowStyleMaskMiniaturizable;
}

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

@ -11,7 +11,6 @@
@protocol AvnWindowProtocol
-(void) pollModalSession: (NSModalSession _Nonnull) session;
-(void) restoreParentWindow;
-(bool) shouldTryToHandleEvents;
-(void) setEnabled: (bool) enable;
-(void) showAppMenuOnly;

15
readme.md

@ -70,11 +70,15 @@ For more information see the [.NET Foundation Code of Conduct](https://dotnetfou
Avalonia is licenced under the [MIT licence](licence.md).
## Support Avalonia
## Donate
**BTC**: bc1q05wx78qemgy9x6ytl5ljk2xrt00yqargyjm8gx
Donating to the project is a fantastic way to thank our valued contributors for their hard work. Your donations are shared among our community and awarded for significant contributions.
If you need support see Commercial Support section below.
Donate with BTC or use [Open Collective](https://opencollective.com/avalonia).
This will be shared with the community and awarded for significant contributions.
**BTC**: bc1q05wx78qemgy9x6ytl5ljk2xrt00yqargyjm8gx
### Backers
@ -98,6 +102,11 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
<a href="https://opencollective.com/Avalonia/sponsor/9/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/9/avatar.svg"></a>
<a href="https://baseheadinc.com/" target="_blank"><img height="50" src="https://baseheadinc.com/wp-content/uploads/2020/09/BH-Logo-for-Site-Header-New.png"></a>
## Commercial Support
We have a range of [support plans available](https://avaloniaui.net/support.html) for those looking to partner with the creators of Avalonia, enabling access to the best support at every step of the development process.
*Please note that donations are not considered payment for commercial support agreements. Please contact us to discuss your needs first. [team@avaloniaui.net](mailto://team@avaloniaui.net)*
## .NET Foundation
This project is supported by the [.NET Foundation](https://dotnetfoundation.org).

35
samples/ControlCatalog.Android/EmbedSample.Android.cs

@ -0,0 +1,35 @@
using System;
using Avalonia.Platform;
using Avalonia.Android;
using ControlCatalog.Pages;
namespace ControlCatalog.Android;
public class EmbedSampleAndroid : INativeDemoControl
{
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
var parentContext = (parent as AndroidViewControlHandle)?.View.Context
?? global::Android.App.Application.Context;
if (isSecond)
{
var webView = new global::Android.Webkit.WebView(parentContext);
webView.LoadUrl("https://www.android.com/");
return new AndroidViewControlHandle(webView);
}
else
{
var button = new global::Android.Widget.Button(parentContext) { Text = "Hello world" };
var clickCount = 0;
button.Click += (sender, args) =>
{
clickCount++;
button.Text = $"Click count {clickCount}";
};
return new AndroidViewControlHandle(button);
}
}
}

6
samples/ControlCatalog.Android/MainActivity.cs

@ -10,7 +10,11 @@ namespace ControlCatalog.Android
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder);
return base.CustomizeAppBuilder(builder)
.AfterSetup(_ =>
{
Pages.EmbedSample.Implementation = new EmbedSampleAndroid();
});
}
}
}

14
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -4,6 +4,7 @@
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(RunNativeAotCompilation)' == 'true'">
@ -12,6 +13,16 @@
<NativeAotCompilerVersion>7.0.0-*</NativeAotCompilerVersion>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\src\Avalonia.X11\NativeDialogs\Gtk.cs" Link="NativeControls\Gtk\Gtk.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="NativeControls\Gtk\nodes.mp4">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" />
@ -20,6 +31,8 @@
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
<!-- For native controls test -->
<PackageReference Include="MonoMac.NetStandard" Version="0.0.4" />
</ItemGroup>
<ItemGroup Condition="'$(RunNativeAotCompilation)' == 'true'">
@ -32,6 +45,7 @@
<PropertyGroup>
<!-- For Microsoft.CodeAnalysis -->
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<Import Project="..\..\build\SampleApp.props" />

35
samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs

@ -0,0 +1,35 @@
using System.IO;
using System.Diagnostics;
using Avalonia.Platform;
using Avalonia.Controls.Platform;
using System;
using ControlCatalog.Pages;
namespace ControlCatalog.NetCore;
public class EmbedSampleGtk : INativeDemoControl
{
private Process _mplayer;
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
if (isSecond)
{
var chooser = GtkHelper.CreateGtkFileChooser(parent.Handle);
if (chooser != null)
return chooser;
}
var control = createDefault();
var nodes = Path.GetFullPath(Path.Combine(typeof(EmbedSample).Assembly.GetModules()[0].FullyQualifiedName,
"..",
"nodes.mp4"));
_mplayer = Process.Start(new ProcessStartInfo("mplayer",
$"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodes}\"")
{
UseShellExecute = false,
});
return control;
}
}

58
samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs

@ -0,0 +1,58 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Platform.Interop;
using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.NativeDialogs.Gtk;
using static Avalonia.X11.NativeDialogs.Glib;
namespace ControlCatalog.NetCore;
internal class GtkHelper
{
private static Task<bool> s_gtkTask;
class FileChooser : INativeControlHostDestroyableControlHandle
{
private readonly IntPtr _widget;
public FileChooser(IntPtr widget, IntPtr xid)
{
_widget = widget;
Handle = xid;
}
public IntPtr Handle { get; }
public string HandleDescriptor => "XID";
public void Destroy()
{
RunOnGlibThread(() =>
{
gtk_widget_destroy(_widget);
return 0;
}).Wait();
}
}
public static INativeControlHostDestroyableControlHandle CreateGtkFileChooser(IntPtr parentXid)
{
if (s_gtkTask == null)
s_gtkTask = StartGtk();
if (!s_gtkTask.Result)
return null;
return RunOnGlibThread(() =>
{
using (var title = new Utf8Buffer("Embedded"))
{
var widget = gtk_file_chooser_dialog_new(title, IntPtr.Zero, GtkFileChooserAction.SelectFolder,
IntPtr.Zero);
gtk_widget_realize(widget);
var xid = gdk_x11_window_get_xid(gtk_widget_get_window(widget));
gtk_window_present(widget);
return new FileChooser(widget, xid);
}
}).Result;
}
}

0
samples/interop/NativeEmbedSample/nodes-license.md → samples/ControlCatalog.NetCore/NativeControls/Gtk/nodes-license.md

0
samples/interop/NativeEmbedSample/nodes.mp4 → samples/ControlCatalog.NetCore/NativeControls/Gtk/nodes.mp4

29
samples/ControlCatalog.NetCore/NativeControls/Mac/EmbedSample.Mac.cs

@ -0,0 +1,29 @@
using System;
using Avalonia.Platform;
using Avalonia.Threading;
using ControlCatalog.Pages;
using MonoMac.Foundation;
using MonoMac.WebKit;
namespace ControlCatalog.NetCore;
public class EmbedSampleMac : INativeDemoControl
{
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
// Note: We are using MonoMac for example purposes
// It shouldn't be used in production apps
MacHelper.EnsureInitialized();
var webView = new WebView();
Dispatcher.UIThread.Post(() =>
{
webView.MainFrame.LoadRequest(new NSUrlRequest(new NSUrl(
isSecond ? "https://bing.com" : "https://google.com/")));
});
return new MacOSViewHandle(webView);
}
}

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

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

45
samples/ControlCatalog.NetCore/NativeControls/Win/EmbedSample.Win.cs

@ -0,0 +1,45 @@
using System;
using System.Text;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using ControlCatalog.Pages;
namespace ControlCatalog.NetCore;
public class EmbedSampleWin : INativeDemoControl
{
private const string RichText =
@"{\rtf1\ansi\ansicpg1251\deff0\nouicompat\deflang1049{\fonttbl{\f0\fnil\fcharset0 Calibri;}}
{\colortbl ;\red255\green0\blue0;\red0\green77\blue187;\red0\green176\blue80;\red155\green0\blue211;\red247\green150\blue70;\red75\green172\blue198;}
{\*\generator Riched20 6.3.9600}\viewkind4\uc1
\pard\sa200\sl276\slmult1\f0\fs22\lang9 <PREFIX>I \i am\i0 a \cf1\b Rich Text \cf0\b0\fs24 control\cf2\fs28 !\cf3\fs32 !\cf4\fs36 !\cf1\fs40 !\cf5\fs44 !\cf6\fs48 !\cf0\fs44\par
}";
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
WinApi.LoadLibrary("Msftedit.dll");
var handle = WinApi.CreateWindowEx(0, "RICHEDIT50W",
@"Rich Edit",
0x800000 | 0x10000000 | 0x40000000 | 0x800000 | 0x10000 | 0x0004, 0, 0, 1, 1, parent.Handle,
IntPtr.Zero, WinApi.GetModuleHandle(null), IntPtr.Zero);
var st = new WinApi.SETTEXTEX { Codepage = 65001, Flags = 0x00000008 };
var text = RichText.Replace("<PREFIX>", isSecond ? "\\qr " : "");
var bytes = Encoding.UTF8.GetBytes(text);
WinApi.SendMessage(handle, 0x0400 + 97, ref st, bytes);
return new Win32WindowControlHandle(handle, "HWND");
}
}
internal class Win32WindowControlHandle : PlatformHandle, INativeControlHostDestroyableControlHandle
{
public Win32WindowControlHandle(IntPtr handle, string descriptor) : base(handle, descriptor)
{
}
public void Destroy()
{
_ = WinApi.DestroyWindow(Handle);
}
}

73
samples/ControlCatalog.NetCore/NativeControls/Win/WinApi.cs

@ -0,0 +1,73 @@
using System;
using System.Runtime.InteropServices;
namespace ControlCatalog.NetCore;
internal unsafe class WinApi
{
public enum CommonControls : uint
{
ICC_LISTVIEW_CLASSES = 0x00000001, // listview, header
ICC_TREEVIEW_CLASSES = 0x00000002, // treeview, tooltips
ICC_BAR_CLASSES = 0x00000004, // toolbar, statusbar, trackbar, tooltips
ICC_TAB_CLASSES = 0x00000008, // tab, tooltips
ICC_UPDOWN_CLASS = 0x00000010, // updown
ICC_PROGRESS_CLASS = 0x00000020, // progress
ICC_HOTKEY_CLASS = 0x00000040, // hotkey
ICC_ANIMATE_CLASS = 0x00000080, // animate
ICC_WIN95_CLASSES = 0x000000FF,
ICC_DATE_CLASSES = 0x00000100, // month picker, date picker, time picker, updown
ICC_USEREX_CLASSES = 0x00000200, // comboex
ICC_COOL_CLASSES = 0x00000400, // rebar (coolbar) control
ICC_INTERNET_CLASSES = 0x00000800,
ICC_PAGESCROLLER_CLASS = 0x00001000, // page scroller
ICC_NATIVEFNTCTL_CLASS = 0x00002000, // native font control
ICC_STANDARD_CLASSES = 0x00004000,
ICC_LINK_CLASS = 0x00008000
}
[StructLayout(LayoutKind.Sequential)]
public struct INITCOMMONCONTROLSEX
{
public int dwSize;
public uint dwICC;
}
[DllImport("Comctl32.dll")]
public static extern void InitCommonControlsEx(ref INITCOMMONCONTROLSEX init);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool DestroyWindow(IntPtr hwnd);
[DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string lib);
[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr CreateWindowEx(
int dwExStyle,
string lpClassName,
string lpWindowName,
uint dwStyle,
int x,
int y,
int nWidth,
int nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
[StructLayout(LayoutKind.Sequential)]
public struct SETTEXTEX
{
public uint Flags;
public uint Codepage;
}
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "SendMessageW")]
public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, ref SETTEXTEX wParam, byte[] lParam);
}

8
samples/ControlCatalog.NetCore/Program.cs

@ -7,11 +7,12 @@ using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Dialogs;
using Avalonia.Headless;
using Avalonia.LogicalTree;
using Avalonia.Threading;
using ControlCatalog.Pages;
namespace ControlCatalog.NetCore
{
static class Program
@ -123,6 +124,11 @@ namespace ControlCatalog.NetCore
{
StartupScreenIndex = 1,
});
EmbedSample.Implementation = OperatingSystem.IsWindows() ? (INativeDemoControl)new EmbedSampleWin()
: OperatingSystem.IsMacOS() ? new EmbedSampleMac()
: OperatingSystem.IsLinux() ? new EmbedSampleGtk()
: null;
})
.LogToTrace();

28
samples/ControlCatalog.NetCore/app.manifest

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="ControlCatalog.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows Vista -->
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
<!-- Windows 7 -->
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
<!-- Windows 8 -->
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
<!-- Windows 8.1 -->
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

4
samples/ControlCatalog.Web/App.razor.cs

@ -7,6 +7,10 @@ public partial class App
protected override void OnParametersSet()
{
WebAppBuilder.Configure<ControlCatalog.App>()
.AfterSetup(_ =>
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
})
.SetupWithSingleViewLifetime();
base.OnParametersSet();

34
samples/ControlCatalog.Web/EmbedSample.Browser.cs

@ -0,0 +1,34 @@
using System;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Web.Blazor;
using ControlCatalog.Pages;
using Microsoft.JSInterop;
namespace ControlCatalog.Web;
public class EmbedSampleWeb : INativeDemoControl
{
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
var runtime = AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>();
if (isSecond)
{
var iframe = runtime.Invoke<IJSInProcessObjectReference>("document.createElement", "iframe");
iframe.InvokeVoid("setAttribute", "src", "https://www.youtube.com/embed/kZCIporjJ70");
return new JSObjectControlHandle(iframe);
}
else
{
// window.createAppButton source is defined in "app.js" file.
var button = runtime.Invoke<IJSInProcessObjectReference>("window.createAppButton");
return new JSObjectControlHandle(button);
}
}
}

70
samples/ControlCatalog.Web/Shared/MainLayout.razor.css

@ -1,70 +0,0 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
.main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
}
.top-row a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row:not(.auth) {
display: none;
}
.top-row.auth {
justify-content: space-between;
}
.top-row a, .top-row .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.main > div {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}

44
samples/ControlCatalog.Web/wwwroot/css/app.css

@ -44,47 +44,13 @@ a, .btn-link {
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.canvas-container {
opacity:1;
background-color:#ccc;
position:fixed;
width:100%;
height:100%;
top:0px;
left:0px;
z-index:500;
}
canvas
{
opacity:1;
background-color:#ccc;
position:fixed;
width:100%;
height:100%;
top:0px;
left:0px;
z-index:500;
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
#app, .page {
height: 100%;
}
.overlay{
opacity:0.0;
background-color:#ccc;
position:fixed;
width:100vw;
height:100vh;
top:0px;
left:0px;
z-index:1000;
}

11
samples/ControlCatalog.Web/wwwroot/js/app.js

@ -1 +1,10 @@

window.createAppButton = function () {
var button = document.createElement('button');
button.innerText = 'Hello world';
var clickCount = 0;
button.onclick = () => {
clickCount++;
button.innerText = 'Click count ' + clickCount;
};
return button;
}

9
samples/ControlCatalog.iOS/AppDelegate.cs

@ -13,6 +13,13 @@ namespace ControlCatalog
[Register("AppDelegate")]
public partial class AppDelegate : AvaloniaAppDelegate<App>
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder)
.AfterSetup(_ =>
{
Pages.EmbedSample.Implementation = new EmbedSampleIOS();
});
}
}
}

38
samples/ControlCatalog.iOS/EmbedSample.iOS.cs

@ -0,0 +1,38 @@
using System;
using Avalonia.Platform;
using CoreGraphics;
using Foundation;
using UIKit;
using WebKit;
using Avalonia.iOS;
using ControlCatalog.Pages;
namespace ControlCatalog;
public class EmbedSampleIOS : INativeDemoControl
{
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
if (isSecond)
{
var webView = new WKWebView(CGRect.Empty, new WKWebViewConfiguration());
webView.LoadRequest(new NSUrlRequest(new NSUrl("https://www.apple.com/")));
return new UIViewControlHandle(webView);
}
else
{
var button = new UIButton();
var clickCount = 0;
button.SetTitle("Hello world", UIControlState.Normal);
button.BackgroundColor = UIColor.Blue;
button.AddTarget((_, _) =>
{
clickCount++;
button.SetTitle($"Click count {clickCount}", UIControlState.Normal);
}, UIControlEvent.TouchDown);
return new UIViewControlHandle(button);
}
}
}

15
samples/ControlCatalog/ControlCatalog.csproj

@ -14,6 +14,9 @@
<AvaloniaResource Include="Assets\*" />
<AvaloniaResource Include="Assets\Fonts\*" />
</ItemGroup>
<ItemGroup>
<None Remove="Pages\NativeEmbedPage.xaml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Bold.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-BoldItalic.ttf" />
@ -32,5 +35,17 @@
<ProjectReference Include="..\SampleControls\ControlSamples.csproj" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Update="Pages\NativeEmbedPage.xaml">
<Generator>MSBuild:Compile</Generator>
</AvaloniaResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Pages\NativeEmbedPage.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

7
samples/ControlCatalog/MainView.xaml

@ -2,8 +2,8 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:ControlSamples;assembly=ControlSamples"
xmlns:pages="clr-namespace:ControlCatalog.Pages"
xmlns:models="clr-namespace:ControlCatalog.Models">
xmlns:models="clr-namespace:ControlCatalog.Models"
xmlns:pages="clr-namespace:ControlCatalog.Pages">
<Grid>
<Grid.Styles>
<Style Selector="TextBlock.h2">
@ -157,6 +157,9 @@
<TabItem Header="Viewbox">
<pages:ViewboxPage />
</TabItem>
<TabItem Header="Native Embed">
<pages:NativeEmbedPage />
</TabItem>
<TabItem Header="Window Customizations">
<pages:WindowCustomizationsPage />
</TabItem>

2
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -20,7 +20,7 @@
Text="Window dialogs" />
<Button Name="DecoratedWindow">Decorated _window</Button>
<Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button>
<Button Name="Dialog">_Dialog</Button>
<Button Name="Dialog" ToolTip.Tip="Shows a dialog">_Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button>
<Button Name="OwnedWindow">Own_ed window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button>

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

@ -151,6 +151,7 @@ namespace ControlCatalog.Pages
private Window CreateSampleWindow()
{
Button button;
Button dialogButton;
var window = new Window
{
@ -167,6 +168,12 @@ namespace ControlCatalog.Pages
HorizontalAlignment = HorizontalAlignment.Center,
Content = "Click to close",
IsDefault = true
}),
(dialogButton = new Button
{
HorizontalAlignment = HorizontalAlignment.Center,
Content = "Dialog",
IsDefault = false
})
}
},
@ -174,6 +181,12 @@ namespace ControlCatalog.Pages
};
button.Click += (_, __) => window.Close();
dialogButton.Click += (_, __) =>
{
var dialog = CreateSampleWindow();
dialog.Height = 200;
dialog.ShowDialog(window);
};
return window;
}

68
samples/ControlCatalog/Pages/NativeEmbedPage.xaml

@ -0,0 +1,68 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="using:ControlCatalog.Pages"
d:DesignHeight="800"
d:DesignWidth="400"
x:Class="ControlCatalog.Pages.NativeEmbedPage">
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="Test">
<MenuItem Header="SubMenu">
<MenuItem Header="Item 1"/>
<MenuItem Header="Item 2"/>
<MenuItem Header="Item 3"/>
</MenuItem>
<MenuItem Header="Item 1"/>
<MenuItem Header="Item 2"/>
<MenuItem Header="Item 3"/>
</MenuItem>
</Menu>
<DockPanel DockPanel.Dock="Top">
<Button DockPanel.Dock="Right" Click="ShowPopupDelay">Show popup (delay)</Button>
<Button DockPanel.Dock="Right" Click="ShowPopup">Show popup</Button>
<Border DockPanel.Dock="Right" Background="#c0c0c0">
<ToolTip.Tip>
<ToolTip>
<TextBlock>Text</TextBlock>
</ToolTip>
</ToolTip.Tip>
<TextBlock VerticalAlignment="Center">Tooltip</TextBlock>
</Border>
<TextBox Text="Lorem ipsum dolor sit amet"/>
</DockPanel>
<Grid ColumnDefinitions="*,5,*"
RowDefinitions="*,5,*">
<Grid.Styles>
<Style Selector="DockPanel#FirstPanel:not(.mobile), DockPanel#SecondPanel:not(.mobile)">
<Setter Property="Grid.RowSpan" Value="3" />
</Style>
<Style Selector="DockPanel#SecondPanel:not(.mobile)">
<Setter Property="Grid.Column" Value="2" />
</Style>
<Style Selector="DockPanel#FirstPanel.mobile, DockPanel#SecondPanel.mobile">
<Setter Property="Grid.ColumnSpan" Value="3" />
</Style>
<Style Selector="DockPanel#SecondPanel.mobile">
<Setter Property="Grid.Row" Value="2" />
</Style>
</Grid.Styles>
<DockPanel x:Name="FirstPanel">
<CheckBox x:Name="firstVisible" DockPanel.Dock="Top"
IsChecked="True" Content="Visible" />
<local:EmbedSample IsVisible="{Binding #firstVisible.IsChecked}"/>
</DockPanel>
<GridSplitter Grid.Row="0" Grid.RowSpan="3" Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />
<GridSplitter Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="1" Height="5" VerticalAlignment="Stretch" />
<DockPanel x:Name="SecondPanel">
<CheckBox x:Name="secondVisible" DockPanel.Dock="Top"
IsChecked="True" Content="Visible" />
<local:EmbedSample IsSecond="True" IsVisible="{Binding #secondVisible.IsChecked}"/>
</DockPanel>
</Grid>
</DockPanel>
</UserControl>

84
samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Platform;
using Avalonia.Interactivity;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Markup.Xaml;
using Avalonia;
namespace ControlCatalog.Pages
{
public class NativeEmbedPage : UserControl
{
public NativeEmbedPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public async void ShowPopupDelay(object sender, RoutedEventArgs args)
{
await Task.Delay(3000);
ShowPopup(sender, args);
}
public void ShowPopup(object sender, RoutedEventArgs args)
{
new ContextMenu()
{
Items = new List<MenuItem>
{
new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" }
}
}.Open((Control)sender);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == BoundsProperty)
{
var isMobile = change.GetNewValue<Rect>().Width < 1200;
this.Find<DockPanel>("FirstPanel")!.Classes.Set("mobile", isMobile);
this.Find<DockPanel>("SecondPanel")!.Classes.Set("mobile", isMobile);
}
}
}
public class EmbedSample : NativeControlHost
{
public static INativeDemoControl? Implementation { get; set; }
static EmbedSample()
{
}
public bool IsSecond { get; set; }
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
{
return Implementation?.CreateControl(IsSecond, parent, () => base.CreateNativeControlCore(parent))
?? base.CreateNativeControlCore(parent);
}
protected override void DestroyNativeControlCore(IPlatformHandle control)
{
base.DestroyNativeControlCore(control);
}
}
public interface INativeDemoControl
{
/// <param name="isSecond">Used to specify which control should be displayed as a demo</param>
IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault);
}
}

8
samples/interop/NativeEmbedSample/App.xaml

@ -1,8 +0,0 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NativeEmbedSample.App">
<Application.Styles>
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
</Application.Styles>
</Application>

22
samples/interop/NativeEmbedSample/App.xaml.cs

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

121
samples/interop/NativeEmbedSample/EmbedSample.cs

@ -1,121 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Controls;
using Avalonia.Platform;
using Avalonia.Threading;
using MonoMac.AppKit;
using MonoMac.Foundation;
using MonoMac.WebKit;
using Encoding = SharpDX.Text.Encoding;
namespace NativeEmbedSample
{
public class EmbedSample : NativeControlHost
{
public bool IsSecond { get; set; }
private Process _mplayer;
IPlatformHandle CreateLinux(IPlatformHandle parent)
{
if (IsSecond)
{
var chooser = GtkHelper.CreateGtkFileChooser(parent.Handle);
if (chooser != null)
return chooser;
}
var control = base.CreateNativeControlCore(parent);
var nodes = Path.GetFullPath(Path.Combine(typeof(EmbedSample).Assembly.GetModules()[0].FullyQualifiedName,
"..",
"nodes.mp4"));
_mplayer = Process.Start(new ProcessStartInfo("mplayer",
$"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodes}\"")
{
UseShellExecute = false,
});
return control;
}
void DestroyLinux(IPlatformHandle handle)
{
_mplayer?.Kill();
_mplayer = null;
base.DestroyNativeControlCore(handle);
}
private const string RichText =
@"{\rtf1\ansi\ansicpg1251\deff0\nouicompat\deflang1049{\fonttbl{\f0\fnil\fcharset0 Calibri;}}
{\colortbl ;\red255\green0\blue0;\red0\green77\blue187;\red0\green176\blue80;\red155\green0\blue211;\red247\green150\blue70;\red75\green172\blue198;}
{\*\generator Riched20 6.3.9600}\viewkind4\uc1
\pard\sa200\sl276\slmult1\f0\fs22\lang9 <PREFIX>I \i am\i0 a \cf1\b Rich Text \cf0\b0\fs24 control\cf2\fs28 !\cf3\fs32 !\cf4\fs36 !\cf1\fs40 !\cf5\fs44 !\cf6\fs48 !\cf0\fs44\par
}";
IPlatformHandle CreateWin32(IPlatformHandle parent)
{
WinApi.LoadLibrary("Msftedit.dll");
var handle = WinApi.CreateWindowEx(0, "RICHEDIT50W",
@"Rich Edit",
0x800000 | 0x10000000 | 0x40000000 | 0x800000 | 0x10000 | 0x0004, 0, 0, 1, 1, parent.Handle,
IntPtr.Zero, WinApi.GetModuleHandle(null), IntPtr.Zero);
var st = new WinApi.SETTEXTEX { Codepage = 65001, Flags = 0x00000008 };
var text = RichText.Replace("<PREFIX>", IsSecond ? "\\qr " : "");
var bytes = Encoding.UTF8.GetBytes(text);
WinApi.SendMessage(handle, 0x0400 + 97, ref st, bytes);
return new PlatformHandle(handle, "HWND");
}
void DestroyWin32(IPlatformHandle handle)
{
WinApi.DestroyWindow(handle.Handle);
}
IPlatformHandle CreateOSX(IPlatformHandle parent)
{
// Note: We are using MonoMac for example purposes
// It shouldn't be used in production apps
MacHelper.EnsureInitialized();
var webView = new WebView();
Dispatcher.UIThread.Post(() =>
{
webView.MainFrame.LoadRequest(new NSUrlRequest(new NSUrl(
IsSecond ? "https://bing.com": "https://google.com/")));
});
return new MacOSViewHandle(webView);
}
void DestroyOSX(IPlatformHandle handle)
{
((MacOSViewHandle)handle).Dispose();
}
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return CreateLinux(parent);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return CreateWin32(parent);
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return CreateOSX(parent);
return base.CreateNativeControlCore(parent);
}
protected override void DestroyNativeControlCore(IPlatformHandle control)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
DestroyLinux(control);
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
DestroyWin32(control);
else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
DestroyOSX(control);
else
base.DestroyNativeControlCore(control);
}
}
}

58
samples/interop/NativeEmbedSample/GtkHelper.cs

@ -1,58 +0,0 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.NativeDialogs.Gtk;
using static Avalonia.X11.NativeDialogs.Glib;
namespace NativeEmbedSample
{
public class GtkHelper
{
private static Task<bool> s_gtkTask;
class FileChooser : INativeControlHostDestroyableControlHandle
{
private readonly IntPtr _widget;
public FileChooser(IntPtr widget, IntPtr xid)
{
_widget = widget;
Handle = xid;
}
public IntPtr Handle { get; }
public string HandleDescriptor => "XID";
public void Destroy()
{
RunOnGlibThread(() =>
{
gtk_widget_destroy(_widget);
return 0;
}).Wait();
}
}
public static IPlatformHandle CreateGtkFileChooser(IntPtr parentXid)
{
if (s_gtkTask == null)
s_gtkTask = StartGtk();
if (!s_gtkTask.Result)
return null;
return RunOnGlibThread(() =>
{
using (var title = new Utf8Buffer("Embedded"))
{
var widget = gtk_file_chooser_dialog_new(title, IntPtr.Zero, GtkFileChooserAction.SelectFolder,
IntPtr.Zero);
gtk_widget_realize(widget);
var xid = gdk_x11_window_get_xid(gtk_widget_get_window(widget));
gtk_window_present(widget);
return new FileChooser(widget, xid);
}
}).Result;
}
}
}

39
samples/interop/NativeEmbedSample/MacHelper.cs

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

52
samples/interop/NativeEmbedSample/MainWindow.xaml

@ -1,52 +0,0 @@
<Window xmlns="https://github.com/avaloniaui" MinWidth="500" MinHeight="300"
Width="1024" Height="800"
Title="Native embedding sample"
xmlns:local="clr-namespace:NativeEmbedSample"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NativeEmbedSample.MainWindow">
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="Test">
<MenuItem Header="SubMenu">
<MenuItem Header="Item 1"/>
<MenuItem Header="Item 2"/>
<MenuItem Header="Item 3"/>
</MenuItem>
<MenuItem Header="Item 1"/>
<MenuItem Header="Item 2"/>
<MenuItem Header="Item 3"/>
</MenuItem>
</Menu>
<DockPanel DockPanel.Dock="Top">
<Button DockPanel.Dock="Right" Click="ShowPopupDelay">Show popup (delay)</Button>
<Button DockPanel.Dock="Right" Click="ShowPopup">Show popup</Button>
<Border DockPanel.Dock="Right" Background="#c0c0c0">
<ToolTip.Tip>
<ToolTip>
<TextBlock>Text</TextBlock>
</ToolTip>
</ToolTip.Tip>
<TextBlock>Tooltip</TextBlock>
</Border>
<TextBox Text="Lorem ipsum dolor sit amet"/>
</DockPanel>
<Grid ColumnDefinitions="*,5,*">
<DockPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
<CheckBox x:Name="firstVisible" IsChecked="True"/>
<TextBlock>Visible</TextBlock>
</StackPanel>
<local:EmbedSample IsVisible="{Binding #firstVisible.IsChecked}"/>
</DockPanel>
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />
<DockPanel Grid.Column="2">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
<CheckBox x:Name="secondVisible" IsChecked="True"/>
<TextBlock>Visible</TextBlock>
</StackPanel>
<local:EmbedSample IsSecond="True" IsVisible="{Binding #secondVisible.IsChecked}"/>
</DockPanel>
</Grid>
</DockPanel>
</Window>

36
samples/interop/NativeEmbedSample/MainWindow.xaml.cs

@ -1,36 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace NativeEmbedSample
{
public class MainWindow : Window
{
public MainWindow()
{
AvaloniaXamlLoader.Load(this);
this.AttachDevTools();
}
public async void ShowPopupDelay(object sender, RoutedEventArgs args)
{
await Task.Delay(3000);
ShowPopup(sender, args);
}
public void ShowPopup(object sender, RoutedEventArgs args)
{
new ContextMenu()
{
Items = new List<MenuItem>
{
new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" }
}
}.Open((Control)sender);
}
}
}

31
samples/interop/NativeEmbedSample/NativeEmbedSample.csproj

@ -1,31 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MonoMac.NetStandard" Version="0.0.4" />
<ProjectReference Include="..\..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
<ProjectReference Include="..\..\..\src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj" />
<ProjectReference Include="..\..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
<AvaloniaResource Include="**\*.xaml">
<SubType>Designer</SubType>
</AvaloniaResource>
<None Remove="nodes.mp4" />
<Content Include="nodes.mp4">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Compile Include="..\..\..\src\Avalonia.X11\NativeDialogs\Gtk.cs" />
</ItemGroup>
<Import Project="..\..\..\build\SampleApp.props" />
<Import Project="..\..\..\build\BuildTargets.targets" />
<Import Project="..\..\..\build\ReferenceCoreLibraries.props" />
</Project>

17
samples/interop/NativeEmbedSample/Program.cs

@ -1,17 +0,0 @@
using Avalonia;
namespace NativeEmbedSample
{
class Program
{
static int Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.With(new AvaloniaNativePlatformOptions()
{
})
.UsePlatformDetect();
}
}

74
samples/interop/NativeEmbedSample/WinApi.cs

@ -1,74 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace NativeEmbedSample
{
public unsafe class WinApi
{
public enum CommonControls : uint
{
ICC_LISTVIEW_CLASSES = 0x00000001, // listview, header
ICC_TREEVIEW_CLASSES = 0x00000002, // treeview, tooltips
ICC_BAR_CLASSES = 0x00000004, // toolbar, statusbar, trackbar, tooltips
ICC_TAB_CLASSES = 0x00000008, // tab, tooltips
ICC_UPDOWN_CLASS = 0x00000010, // updown
ICC_PROGRESS_CLASS = 0x00000020, // progress
ICC_HOTKEY_CLASS = 0x00000040, // hotkey
ICC_ANIMATE_CLASS = 0x00000080, // animate
ICC_WIN95_CLASSES = 0x000000FF,
ICC_DATE_CLASSES = 0x00000100, // month picker, date picker, time picker, updown
ICC_USEREX_CLASSES = 0x00000200, // comboex
ICC_COOL_CLASSES = 0x00000400, // rebar (coolbar) control
ICC_INTERNET_CLASSES = 0x00000800,
ICC_PAGESCROLLER_CLASS = 0x00001000, // page scroller
ICC_NATIVEFNTCTL_CLASS = 0x00002000, // native font control
ICC_STANDARD_CLASSES = 0x00004000,
ICC_LINK_CLASS = 0x00008000
}
[StructLayout(LayoutKind.Sequential)]
public struct INITCOMMONCONTROLSEX
{
public int dwSize;
public uint dwICC;
}
[DllImport("Comctl32.dll")]
public static extern void InitCommonControlsEx(ref INITCOMMONCONTROLSEX init);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool DestroyWindow(IntPtr hwnd);
[DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string lib);
[DllImport("kernel32.dll")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr CreateWindowEx(
int dwExStyle,
string lpClassName,
string lpWindowName,
uint dwStyle,
int x,
int y,
int nWidth,
int nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
[StructLayout(LayoutKind.Sequential)]
public struct SETTEXTEX
{
public uint Flags;
public uint Codepage;
}
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "SendMessageW")]
public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, ref SETTEXTEX wParam, byte[] lParam);
}
}

32
src/Android/Avalonia.Android/AndroidViewControlHandle.cs

@ -0,0 +1,32 @@
#nullable enable
using System;
using Android.Views;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
namespace Avalonia.Android
{
public class AndroidViewControlHandle : INativeControlHostDestroyableControlHandle
{
internal const string AndroidDescriptor = "JavaObjectHandle";
public AndroidViewControlHandle(View view)
{
View = view;
}
public View View { get; }
public string HandleDescriptor => AndroidDescriptor;
IntPtr IPlatformHandle.Handle => View.Handle;
public void Destroy()
{
View?.Dispose();
}
}
}

7
src/Android/Avalonia.Android/AvaloniaView.cs

@ -19,9 +19,8 @@ namespace Avalonia.Android
public AvaloniaView(Context context) : base(context)
{
_view = new ViewImpl(context);
_view = new ViewImpl(this);
AddView(_view.View);
}
internal void Prepare ()
@ -30,6 +29,8 @@ namespace Avalonia.Android
_root.Prepare();
}
internal TopLevelImpl TopLevelImpl => _view;
public object Content
{
get { return _root.Content; }
@ -73,7 +74,7 @@ namespace Avalonia.Android
class ViewImpl : TopLevelImpl
{
public ViewImpl(Context context) : base(context)
public ViewImpl(AvaloniaView avaloniaView) : base(avaloniaView)
{
View.Focusable = true;
View.FocusChange += ViewImpl_FocusChange;

139
src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs

@ -0,0 +1,139 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using Android.Views;
using Android.Widget;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
namespace Avalonia.Android.Platform
{
internal class AndroidNativeControlHostImpl : INativeControlHostImpl
{
private readonly AvaloniaView _avaloniaView;
public AndroidNativeControlHostImpl(AvaloniaView avaloniaView)
{
_avaloniaView = avaloniaView;
}
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent)
{
return new AndroidViewControlHandle(new FrameLayout(_avaloniaView.Context!));
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create)
{
var parent = new AndroidViewControlHandle(_avaloniaView);
AndroidNativeControlAttachment? attachment = null;
try
{
var child = create(parent);
// It has to be assigned to the variable before property setter is called so we dispose it on exception
#pragma warning disable IDE0017 // Simplify object initialization
attachment = new AndroidNativeControlAttachment(child);
#pragma warning restore IDE0017 // Simplify object initialization
attachment.AttachedTo = this;
return attachment;
}
catch
{
attachment?.Dispose();
throw;
}
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle)
{
return new AndroidNativeControlAttachment(handle)
{
AttachedTo = this
};
}
public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == AndroidViewControlHandle.AndroidDescriptor;
private class AndroidNativeControlAttachment : INativeControlHostControlTopLevelAttachment
{
private View? _view;
private AndroidNativeControlHostImpl? _attachedTo;
public AndroidNativeControlAttachment(IPlatformHandle child)
{
_view = (child as AndroidViewControlHandle)?.View
?? Java.Lang.Object.GetObject<View>(child.Handle, global::Android.Runtime.JniHandleOwnership.DoNotTransfer);
}
[MemberNotNull(nameof(_view))]
private void CheckDisposed()
{
if (_view == null)
throw new ObjectDisposedException(nameof(AndroidNativeControlAttachment));
}
public void Dispose()
{
if (_view != null && _attachedTo?._avaloniaView is ViewGroup parent)
{
parent.RemoveView(_view);
}
_attachedTo = null;
_view?.Dispose();
_view = null;
}
public INativeControlHostImpl? AttachedTo
{
get => _attachedTo;
set
{
CheckDisposed();
var oldAttachedTo = _attachedTo;
_attachedTo = (AndroidNativeControlHostImpl?)value;
if (_attachedTo == null)
{
oldAttachedTo?._avaloniaView.RemoveView(_view);
}
else
{
_attachedTo._avaloniaView.AddView(_view);
}
}
}
public bool IsCompatibleWith(INativeControlHostImpl host) => host is AndroidNativeControlHostImpl;
public void HideWithSize(Size size)
{
CheckDisposed();
if (_attachedTo == null)
return;
size *= _attachedTo._avaloniaView.TopLevelImpl.RenderScaling;
_view.Visibility = ViewStates.Gone;
_view.LayoutParameters = new FrameLayout.LayoutParams(Math.Max(1, (int)size.Width), Math.Max(1, (int)size.Height));
_view.RequestLayout();
}
public void ShowInBounds(Rect bounds)
{
CheckDisposed();
if (_attachedTo == null)
throw new InvalidOperationException("The control isn't currently attached to a toplevel");
bounds *= _attachedTo._avaloniaView.TopLevelImpl.RenderScaling;
_view.Visibility = ViewStates.Visible;
_view.LayoutParameters = new FrameLayout.LayoutParams(Math.Max(1, (int)bounds.Width), Math.Max(1, (int)bounds.Height))
{
LeftMargin = (int)bounds.X,
TopMargin = (int)bounds.Y
};
_view.RequestLayout();
}
}
}
}

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

@ -20,7 +20,7 @@ using Avalonia.Rendering;
namespace Avalonia.Android.Platform.SkiaPlatform
{
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost
{
private readonly IGlPlatformSurface _gl;
private readonly IFramebufferPlatformSurface _framebuffer;
@ -30,9 +30,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly ITextInputMethodImpl _textInputMethod;
private ViewImpl _view;
public TopLevelImpl(Context context, bool placeOnTop = false)
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
{
_view = new ViewImpl(context, this, placeOnTop);
_view = new ViewImpl(avaloniaView.Context, this, placeOnTop);
_textInputMethod = new AndroidInputMethod<ViewImpl>(_view);
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
_touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(this, () => InputRoot,
@ -44,6 +44,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
_view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
}
public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
@ -222,6 +224,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public ITextInputMethodImpl TextInputMethod => _textInputMethod;
public INativeControlHostImpl NativeControlHost { get; }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{
throw new NotImplementedException();

9
src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs

@ -33,7 +33,14 @@ namespace Avalonia.Data.Converters
if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1)
{
return new MethodToCommandConverter(d);
if (d.Method.IsPrivate == false)
{
return new MethodToCommandConverter(d);
}
else
{
return new BindingNotification(new InvalidCastException("You can't bind to private methods!"), BindingErrorType.Error);
}
}
if (TypeUtilities.TryConvert(targetType, value, culture, out var result))

11
src/Avalonia.Base/Media/TextFormatting/TextBounds.cs

@ -10,20 +10,27 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Constructing TextBounds object
/// </summary>
internal TextBounds(Rect bounds, FlowDirection flowDirection)
internal TextBounds(Rect bounds, FlowDirection flowDirection, IList<TextRunBounds> runBounds)
{
Rectangle = bounds;
FlowDirection = flowDirection;
TextRunBounds = runBounds;
}
/// <summary>
/// Bounds rectangle
/// </summary>
public Rect Rectangle { get; }
public Rect Rectangle { get; internal set; }
/// <summary>
/// Text flow direction inside the boundary rectangle
/// </summary>
public FlowDirection FlowDirection { get; }
/// <summary>
/// Get a list of run bounding rectangles
/// </summary>
/// <returns>Array of text run bounds</returns>
public IList<TextRunBounds> TextRunBounds { get; }
}
}

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

@ -230,7 +230,7 @@ namespace Avalonia.Media.TextFormatting
foreach (var textLine in TextLines)
{
//Current line isn't covered.
if (textLine.FirstTextSourceIndex + textLine.Length <= start)
if (textLine.FirstTextSourceIndex + textLine.Length < start)
{
currentY += textLine.Height;
@ -239,18 +239,27 @@ namespace Avalonia.Media.TextFormatting
var textBounds = textLine.GetTextBounds(start, length);
foreach (var bounds in textBounds)
if(textBounds.Count > 0)
{
Rect? last = result.Count > 0 ? result[result.Count - 1] : null;
if (last.HasValue && MathUtilities.AreClose(last.Value.Right, bounds.Rectangle.Left) && MathUtilities.AreClose(last.Value.Top, currentY))
foreach (var bounds in textBounds)
{
result[result.Count - 1] = last.Value.WithWidth(last.Value.Width + bounds.Rectangle.Width);
Rect? last = result.Count > 0 ? result[result.Count - 1] : null;
if (last.HasValue && MathUtilities.AreClose(last.Value.Right, bounds.Rectangle.Left) && MathUtilities.AreClose(last.Value.Top, currentY))
{
result[result.Count - 1] = last.Value.WithWidth(last.Value.Width + bounds.Rectangle.Width);
}
else
{
result.Add(bounds.Rectangle.WithY(currentY));
}
foreach (var runBounds in bounds.TextRunBounds)
{
start += runBounds.Length;
length -= runBounds.Length;
}
}
else
{
result.Add(bounds.Rectangle.WithY(currentY));
}
}
if(textLine.FirstTextSourceIndex + textLine.Length >= start + length)

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

@ -184,6 +184,10 @@ namespace Avalonia.Media.TextFormatting
{
characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
var offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
break;
}
default:
@ -215,9 +219,11 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0);
var isTrailingHit = characterHit.TrailingLength > 0;
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var currentDistance = Start;
var currentPosition = FirstTextSourceIndex;
var remainingLength = characterIndex - FirstTextSourceIndex;
GlyphRun? lastRun = null;
@ -242,8 +248,10 @@ namespace Avalonia.Media.TextFormatting
}
//Look for a hit in within the current run
if (characterIndex >= textRun.Text.Start && characterIndex <= textRun.Text.Start + textRun.Text.Length)
if (currentPosition + remainingLength <= currentPosition + textRun.Text.Length)
{
characterHit = new CharacterHit(textRun.Text.Start + remainingLength);
var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
return currentDistance + distance;
@ -254,28 +262,27 @@ namespace Avalonia.Media.TextFormatting
{
if (_flowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight))
{
if (characterIndex <= textRun.Text.Start)
if (characterIndex <= currentPosition)
{
return currentDistance;
}
}
else
{
if (characterIndex == textRun.Text.Start)
if (characterIndex == currentPosition)
{
return currentDistance;
}
}
if (characterIndex == textRun.Text.Start + textRun.Text.Length &&
characterHit.TrailingLength > 0)
if (characterIndex == currentPosition + textRun.Text.Length && isTrailingHit)
{
return currentDistance + currentRun.Size.Width;
}
}
else
{
if (characterIndex == textRun.Text.Start)
if (characterIndex == currentPosition)
{
return currentDistance + currentRun.Size.Width;
}
@ -286,20 +293,24 @@ namespace Avalonia.Media.TextFormatting
if (nextRun != null)
{
if (characterHit.FirstCharacterIndex == textRun.Text.End &&
nextRun.ShapedBuffer.IsLeftToRight)
if (nextRun.ShapedBuffer.IsLeftToRight)
{
return currentDistance;
if (characterIndex == currentPosition + textRun.Text.Length)
{
return currentDistance;
}
}
if (characterIndex > textRun.Text.End && nextRun.Text.End < textRun.Text.End)
else
{
return currentDistance;
if (currentPosition + nextRun.Text.Length == characterIndex)
{
return currentDistance;
}
}
}
else
{
if (characterIndex > textRun.Text.End)
if (characterIndex > currentPosition + textRun.Text.Length)
{
return currentDistance;
}
@ -329,6 +340,12 @@ namespace Avalonia.Media.TextFormatting
//No hit hit found so we add the full width
currentDistance += textRun.Size.Width;
currentPosition += textRun.TextSourceLength;
remainingLength -= textRun.TextSourceLength;
if (remainingLength <= 0)
{
break;
}
}
return currentDistance;
@ -394,210 +411,299 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit);
}
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceCharacterIndex, int textLength)
private IReadOnlyList<TextBounds> GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength)
{
if (firstTextSourceCharacterIndex + textLength <= FirstTextSourceIndex)
{
return Array.Empty<TextBounds>();
}
var characterIndex = firstTextSourceIndex + textLength;
var result = new List<TextBounds>(TextRuns.Count);
var lastDirection = _flowDirection;
var lastDirection = FlowDirection.LeftToRight;
var currentDirection = lastDirection;
var currentPosition = FirstTextSourceIndex;
var currentRect = Rect.Empty;
var remainingLength = textLength;
var startX = Start;
double currentWidth = 0;
var currentRect = Rect.Empty;
//A portion of the line is covered.
for (var index = 0; index < TextRuns.Count; index++)
{
var currentRun = TextRuns[index] as DrawableTextRun;
if (currentRun is null)
if (TextRuns[index] is not DrawableTextRun currentRun)
{
continue;
}
TextRun? nextRun = null;
if (index + 1 < TextRuns.Count)
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
nextRun = TextRuns[index + 1];
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
continue;
}
if (nextRun != null)
var characterLength = 0;
var endX = startX;
if (currentRun is ShapedTextCharacters currentShapedRun)
{
switch (nextRun)
{
case ShapedTextCharacters when currentRun is ShapedTextCharacters:
{
if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
{
goto skip;
}
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
{
goto skip;
}
currentPosition += offset;
if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
{
goto skip;
}
var startIndex = currentRun.Text.Start + offset;
if (currentRun.Text.End < firstTextSourceCharacterIndex)
{
goto skip;
}
var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
currentShapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(startIndex + remainingLength) :
new CharacterHit(startIndex));
goto noop;
}
default:
{
goto noop;
}
}
endX += endOffset;
var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
currentShapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(startIndex) :
new CharacterHit(startIndex + remainingLength));
startX += startOffset;
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
skip:
currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ?
FlowDirection.LeftToRight :
FlowDirection.RightToLeft;
}
else
{
if (currentPosition < firstTextSourceIndex)
{
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
}
continue;
noop:
if (currentPosition + currentRun.TextSourceLength <= characterIndex)
{
endX += currentRun.Size.Width;
characterLength = currentRun.TextSourceLength;
}
}
var endX = startX;
var endOffset = 0d;
if (endX < startX)
{
(endX, startX) = (startX, endX);
}
switch (currentRun)
//Lines that only contain a linebreak need to be covered here
if(characterLength == 0)
{
case ShapedTextCharacters shapedRun:
{
endOffset = shapedRun.GlyphRun.GetDistanceFromCharacterHit(
shapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(firstTextSourceCharacterIndex + textLength) :
new CharacterHit(firstTextSourceCharacterIndex));
characterLength = NewLineLength;
}
endX += endOffset;
var runwidth = endX - startX;
var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runwidth, Height), currentPosition, characterLength, currentRun);
var startOffset = shapedRun.GlyphRun.GetDistanceFromCharacterHit(
shapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(firstTextSourceCharacterIndex) :
new CharacterHit(firstTextSourceCharacterIndex + textLength));
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
currentRect = currentRect.WithWidth(currentWidth + runwidth);
startX += startOffset;
var textBounds = result[result.Count - 1];
var characterHit = shapedRun.GlyphRun.IsLeftToRight ?
shapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _) :
shapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
textBounds.Rectangle = currentRect;
currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
currentDirection = shapedRun.ShapedBuffer.IsLeftToRight ?
FlowDirection.LeftToRight :
FlowDirection.RightToLeft;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
if (nextRun is ShapedTextCharacters nextShaped)
{
if (shapedRun.ShapedBuffer.IsLeftToRight == nextShaped.ShapedBuffer.IsLeftToRight)
{
endOffset = nextShaped.GlyphRun.GetDistanceFromCharacterHit(
nextShaped.ShapedBuffer.IsLeftToRight ?
new CharacterHit(firstTextSourceCharacterIndex + textLength) :
new CharacterHit(firstTextSourceCharacterIndex));
currentWidth += runwidth;
currentPosition += characterLength;
index++;
if (currentDirection == FlowDirection.LeftToRight)
{
if (currentPosition > characterIndex)
{
break;
}
}
else
{
if (currentPosition <= firstTextSourceIndex)
{
break;
}
}
endX += endOffset;
startX = endX;
lastDirection = currentDirection;
remainingLength -= characterLength;
currentRun = nextShaped;
if (remainingLength <= 0)
{
break;
}
}
if (nextShaped.ShapedBuffer.IsLeftToRight)
{
characterHit = nextShaped.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
return result;
}
currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
}
}
}
private IReadOnlyList<TextBounds> GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength)
{
var characterIndex = firstTextSourceIndex + textLength;
break;
}
default:
{
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex + textLength)
{
endX += currentRun.Size.Width;
}
var result = new List<TextBounds>(TextRuns.Count);
var lastDirection = FlowDirection.LeftToRight;
var currentDirection = lastDirection;
if (currentPosition < firstTextSourceCharacterIndex)
{
startX += currentRun.Size.Width;
}
var currentPosition = FirstTextSourceIndex;
var remainingLength = textLength;
currentPosition += currentRun.TextSourceLength;
var startX = Start + WidthIncludingTrailingWhitespace;
double currentWidth = 0;
var currentRect = Rect.Empty;
break;
}
for (var index = TextRuns.Count - 1; index >= 0; index--)
{
if (TextRuns[index] is not DrawableTextRun currentRun)
{
continue;
}
if (endX < startX)
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
(endX, startX) = (startX, endX);
startX -= currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
continue;
}
var width = endX - startX;
var characterLength = 0;
var endX = startX;
if (!MathUtilities.IsZero(width))
if (currentRun is ShapedTextCharacters currentShapedRun)
{
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
currentRect = currentRect.WithWidth(currentRect.Width + width);
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
currentPosition += offset;
var startIndex = currentRun.Text.Start + offset;
var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
currentShapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(startIndex + remainingLength) :
new CharacterHit(startIndex));
endX += endOffset - currentShapedRun.Size.Width;
var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
currentShapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(startIndex) :
new CharacterHit(startIndex + remainingLength));
var textBounds = new TextBounds(currentRect, currentDirection);
startX += startOffset - currentShapedRun.Size.Width;
result[result.Count - 1] = textBounds;
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ?
FlowDirection.LeftToRight :
FlowDirection.RightToLeft;
}
else
{
if (currentPosition + currentRun.TextSourceLength <= characterIndex)
{
endX -= currentRun.Size.Width;
}
else
if (currentPosition < firstTextSourceIndex)
{
startX -= currentRun.Size.Width;
characterLength = currentRun.TextSourceLength;
}
}
if (endX < startX)
{
(endX, startX) = (startX, endX);
}
currentRect = new Rect(startX, 0, width, Height);
//Lines that only contain a linebreak need to be covered here
if (characterLength == 0)
{
characterLength = NewLineLength;
}
result.Add(new TextBounds(currentRect, currentDirection));
var runWidth = endX - startX;
var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
}
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
currentRect = currentRect.WithWidth(currentWidth + runWidth);
var textBounds = result[result.Count - 1];
textBounds.Rectangle = currentRect;
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
currentWidth += runWidth;
currentPosition += characterLength;
if (currentDirection == FlowDirection.LeftToRight)
{
if (currentPosition > firstTextSourceCharacterIndex + textLength)
if (currentPosition > characterIndex)
{
break;
}
}
else
{
if (currentPosition <= firstTextSourceCharacterIndex)
if (currentPosition <= firstTextSourceIndex)
{
break;
}
endX += currentRun.Size.Width - endOffset;
}
lastDirection = currentDirection;
startX = endX;
remainingLength -= characterLength;
if (remainingLength <= 0)
{
break;
}
}
return result;
}
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
{
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{
return GetTextBoundsLeftToRight(firstTextSourceIndex, textLength);
}
return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength);
}
public TextLineImpl FinalizeLine()
{
_textLineMetrics = CreateLineMetrics();

39
src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs

@ -0,0 +1,39 @@
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// The bounding rectangle of text run
/// </summary>
public sealed class TextRunBounds
{
/// <summary>
/// Constructing TextRunBounds
/// </summary>
internal TextRunBounds(Rect bounds, int firstCharacterIndex, int length, TextRun textRun)
{
Rectangle = bounds;
TextSourceCharacterIndex = firstCharacterIndex;
Length = length;
TextRun = textRun;
}
/// <summary>
/// First text source character index of text run
/// </summary>
public int TextSourceCharacterIndex { get; }
/// <summary>
/// character length of bounded text run
/// </summary>
public int Length { get; }
/// <summary>
/// Text run bounding rectangle
/// </summary>
public Rect Rectangle { get; }
/// <summary>
/// text run
/// </summary>
public TextRun TextRun { get; }
}
}

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

@ -16,7 +16,7 @@ namespace Avalonia.Media.TextFormatting
{
Typeface = typeface;
FontRenderingEmSize = fontRenderingEmSize;
BidLevel = bidiLevel;
BidiLevel = bidiLevel;
Culture = culture;
IncrementalTabWidth = incrementalTabWidth;
}
@ -33,7 +33,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Get the bidi level of the text.
/// </summary>
public sbyte BidLevel { get; }
public sbyte BidiLevel { get; }
/// <summary>
/// Get the culture.

4
src/Avalonia.Base/Styling/Style.cs

@ -94,7 +94,6 @@ namespace Avalonia.Styling
/// <summary>
/// Gets the style's setters.
/// </summary>
[Content]
public IList<ISetter> Setters => _setters ??= new List<ISetter>();
/// <summary>
@ -107,6 +106,9 @@ namespace Avalonia.Styling
public event EventHandler? OwnerChanged;
public void Add(ISetter setter) => Setters.Add(setter);
public void Add(IStyle style) => Children.Add(style);
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{
target = target ?? throw new ArgumentNullException(nameof(target));

5
src/Avalonia.Controls/ContextMenu.cs

@ -450,6 +450,11 @@ namespace Avalonia.Controls
if (sender is Control control
&& control.ContextMenu is ContextMenu contextMenu)
{
if (contextMenu._popup?.Parent == control)
{
((ISetLogicalParent)contextMenu._popup).SetParent(null);
}
contextMenu.Close();
}
}

5
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@ -175,7 +175,8 @@ namespace Avalonia.Controls.Primitives
IsOpen = false;
Popup.IsOpen = false;
((ISetLogicalParent)Popup).SetParent(null);
// Ensure this isn't active
_transientDisposable?.Dispose();
_transientDisposable = null;
@ -218,7 +219,7 @@ namespace Avalonia.Controls.Primitives
((ISetLogicalParent)Popup).SetParent(null);
}
if (Popup.PlacementTarget != placementTarget)
if (Popup.Parent == null || Popup.PlacementTarget != placementTarget)
{
Popup.PlacementTarget = Target = placementTarget;
((ISetLogicalParent)Popup).SetParent(placementTarget);

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

@ -530,11 +530,6 @@ namespace Avalonia.Controls.Presenters
protected override Size MeasureOverride(Size availableSize)
{
if (string.IsNullOrEmpty(Text))
{
return new Size();
}
_constraint = availableSize;
_textLayout = null;

6
src/Avalonia.Controls/TextBlock.cs

@ -631,7 +631,11 @@ namespace Avalonia.Controls
return finalSize;
}
_constraint = new Size(finalSize.Width, double.PositiveInfinity);
var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
_constraint = new Size(finalSize.Deflate(padding).Width, double.PositiveInfinity);
_textLayout = null;

2
src/Avalonia.Controls/Viewbox.cs

@ -168,6 +168,8 @@ namespace Avalonia.Controls
if (_child is not null)
VisualChildren.Add(_child);
InvalidateMeasure();
}
}
}

4
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -14,6 +14,10 @@
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\DevAnalyzers.props" />
</Project>

4
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -8,7 +8,7 @@ namespace Avalonia.Dialogs
{
public static class ManagedFileDialogExtensions
{
private class ManagedSystemDialogImpl<T> : ISystemDialogImpl where T : Window, new()
internal class ManagedSystemDialogImpl<T> : ISystemDialogImpl where T : Window, new()
{
async Task<string[]> Show(SystemDialog d, Window parent, ManagedFileDialogOptions options = null)
{
@ -141,7 +141,7 @@ namespace Avalonia.Dialogs
public static Task<string[]> ShowManagedAsync(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions options = null) => ShowManagedAsync<Window>(dialog, parent, options);
public static Task<string[]> ShowManagedAsync<TWindow>(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions options = null) where TWindow : Window, new()
{

2
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -2,10 +2,12 @@
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<PackageReference Include="Tmds.DBus" Version="0.9.0" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">

32
src/Avalonia.FreeDesktop/DBusFileChooser.cs

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
namespace Avalonia.FreeDesktop
{
[DBusInterface("org.freedesktop.portal.FileChooser")]
internal interface IFileChooser : IDBusObject
{
Task<ObjectPath> OpenFileAsync(string ParentWindow, string Title, IDictionary<string, object> Options);
Task<ObjectPath> SaveFileAsync(string ParentWindow, string Title, IDictionary<string, object> Options);
Task<ObjectPath> SaveFilesAsync(string ParentWindow, string Title, IDictionary<string, object> Options);
Task<T> GetAsync<T>(string prop);
Task<FileChooserProperties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
[Dictionary]
internal class FileChooserProperties
{
public uint Version { get; set; }
}
internal static class FileChooserExtensions
{
public static Task<uint> GetVersionAsync(this IFileChooser o) => o.GetAsync<uint>("version");
}
}

24
src/Avalonia.FreeDesktop/DBusHelper.cs

@ -6,7 +6,7 @@ using Tmds.DBus;
namespace Avalonia.FreeDesktop
{
public class DBusHelper
public static class DBusHelper
{
/// <summary>
/// This class uses synchronous execution at DBus connection establishment stage
@ -14,14 +14,14 @@ namespace Avalonia.FreeDesktop
/// </summary>
private class DBusSyncContext : SynchronizationContext
{
private SynchronizationContext _ctx;
private object _lock = new object();
private readonly object _lock = new();
private SynchronizationContext? _ctx;
public override void Post(SendOrPostCallback d, object state)
{
lock (_lock)
{
if (_ctx != null)
if (_ctx is not null)
_ctx?.Post(d, state);
else
lock (_lock)
@ -33,10 +33,9 @@ namespace Avalonia.FreeDesktop
{
lock (_lock)
{
if (_ctx != null)
if (_ctx is not null)
_ctx?.Send(d, state);
else
d(state);
}
}
@ -47,15 +46,14 @@ namespace Avalonia.FreeDesktop
_ctx = new AvaloniaSynchronizationContext();
}
}
public static Connection Connection { get; private set; }
public static Connection TryInitialize(string dbusAddress = null)
public static Connection? Connection { get; private set; }
public static Connection? TryInitialize(string? dbusAddress = null)
=> Connection ?? TryCreateNewConnection(dbusAddress);
public static Connection? TryCreateNewConnection(string? dbusAddress = null)
{
return Connection ?? TryCreateNewConnection(dbusAddress);
}
public static Connection TryCreateNewConnection(string dbusAddress = null)
{
var oldContext = SynchronizationContext.Current;
try
{

16
src/Avalonia.FreeDesktop/DBusRequest.cs

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
namespace Avalonia.FreeDesktop
{
[DBusInterface("org.freedesktop.portal.Request")]
internal interface IRequest : IDBusObject
{
Task CloseAsync();
Task<IDisposable> WatchResponseAsync(Action<(uint response, IDictionary<string, object> results)> handler, Action<Exception> onError = null);
}
}

102
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Logging;
using Tmds.DBus;
namespace Avalonia.FreeDesktop
{
internal class DBusSystemDialog : ISystemDialogImpl
{
private readonly IFileChooser _fileChooser;
internal static DBusSystemDialog? TryCreate()
{
var fileChooser = DBusHelper.Connection?.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
if (fileChooser is null)
return null;
try
{
fileChooser.GetVersionAsync().GetAwaiter().GetResult();
return new DBusSystemDialog(fileChooser);
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}");
return null;
}
}
private DBusSystemDialog(IFileChooser fileChooser)
{
_fileChooser = fileChooser;
}
public async Task<string[]?> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}";
ObjectPath objectPath;
var options = new Dictionary<string, object>();
if (dialog.Filters is not null)
options.Add("filters", ParseFilters(dialog));
switch (dialog)
{
case OpenFileDialog openFileDialog:
options.Add("multiple", openFileDialog.AllowMultiple);
objectPath = await _fileChooser.OpenFileAsync(parentWindow, openFileDialog.Title ?? string.Empty, options);
break;
case SaveFileDialog saveFileDialog:
if (saveFileDialog.InitialFileName is not null)
options.Add("current_name", saveFileDialog.InitialFileName);
if (saveFileDialog.Directory is not null)
options.Add("current_folder", Encoding.UTF8.GetBytes(saveFileDialog.Directory));
objectPath = await _fileChooser.SaveFileAsync(parentWindow, saveFileDialog.Title ?? string.Empty, options);
break;
}
var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
var uris = await tsc.Task;
if (uris is null)
return null;
for (var i = 0; i < uris.Length; i++)
uris[i] = new Uri(uris[i]).AbsolutePath;
return uris;
}
public async Task<string?> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}";
var options = new Dictionary<string, object>
{
{ "directory", true }
};
var objectPath = await _fileChooser.OpenFileAsync(parentWindow, dialog.Title ?? string.Empty, options);
var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
var uris = await tsc.Task;
if (uris is null)
return null;
return uris.Length != 1 ? string.Empty : new Uri(uris[0]).AbsolutePath;
}
private static (string name, (uint style, string extension)[])[] ParseFilters(FileDialog dialog)
{
var filters = new (string name, (uint style, string extension)[])[dialog.Filters!.Count];
for (var i = 0; i < filters.Length; i++)
{
var extensions = dialog.Filters[i].Extensions.Select(static x => (0u, x)).ToArray();
filters[i] = (dialog.Filters[i].Name ?? string.Empty, extensions);
}
return filters;
}
}
}

2
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -137,7 +137,7 @@ namespace Avalonia.Headless
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel;
var bidiLevel = options.BidiLevel;
return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
}

74
src/Avalonia.Themes.Fluent/Controls/Button.xaml

@ -40,53 +40,49 @@
</ControlTemplate>
</Setter>
<Style.Children>
<Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
<Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
<Style Selector="^.accent">
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" />
</Style>
<Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPressed}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" />
</Style>
<Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
<Style Selector="^.accent">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" />
</Style>
<Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPressed}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" />
</Style>
<Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />
</Style>
</Style.Children>
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />
</Style>
</Style.Children>
</Style>
</Style>
<Style Selector="Button, RepeatButton, ToggleButton, DropDownButton">

464
src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml

@ -56,266 +56,242 @@
</ControlTemplate>
</Setter>
<Style.Children>
<!-- Unchecked Normal State -->
<!-- Unchecked Normal State -->
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUnchecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUnchecked}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUnchecked}" />
<Setter Property="Opacity" Value="0" />
</Style>
<!-- Unchecked PointerOver State -->
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedPointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedPointerOver}" />
</Style>
</Style>
<!-- Unchecked Pressed State -->
<Style Selector="^:pressed">
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPressed}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedPressed}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUnchecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUnchecked}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedPressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedPressed}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUnchecked}" />
<Setter Property="Opacity" Value="0" />
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedPressed}" />
</Style>
</Style>
<!-- Unchecked PointerOver State -->
<!-- Unchecked Disabled state -->
<Style Selector="^:disabled">
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedDisabled}" />
</Style>
</Style>
<Style Selector="^:checked">
<!-- Checked Normal State -->
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundChecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundChecked}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushChecked}" />
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundFillChecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillChecked}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundChecked}" />
<Setter Property="Data" Value="M1507 31L438 1101L-119 543L-29 453L438 919L1417 -59L1507 31Z" />
<Setter Property="Width" Value="9" />
<Setter Property="Opacity" Value="1" />
<Setter Property="FlowDirection" Value="LeftToRight" />
</Style>
<!-- Checked PointerOver State -->
<Style Selector="^:pointerover">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedPointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedPointerOver}" />
</Style>
</Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedPointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedPointerOver}" />
</Style>
</Style>
<!-- Unchecked Pressed State -->
<!-- Checked Pressed State -->
<Style Selector="^:pressed">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPressed}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedPressed}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedPressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedPressed}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedPressed}" />
</Style>
</Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPressed}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedPressed}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedPressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedPressed}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedPressed}" />
</Style>
</Style>
<!-- Unchecked Disabled state -->
<!-- Checked Disabled State -->
<Style Selector="^:disabled">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedDisabled}" />
</Style>
</Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedDisabled}" />
</Style>
</Style>
</Style>
<Style Selector="^:checked">
<!-- Checked Normal State -->
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundChecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundChecked}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushChecked}" />
<Style.Children>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundFillChecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillChecked}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundChecked}" />
<Setter Property="Data" Value="M1507 31L438 1101L-119 543L-29 453L438 919L1417 -59L1507 31Z" />
<Setter Property="Width" Value="9" />
<Setter Property="Opacity" Value="1" />
<Setter Property="FlowDirection" Value="LeftToRight" />
</Style>
<!-- Checked PointerOver State -->
<Style Selector="^:pointerover">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedPointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedPointerOver}" />
</Style>
</Style.Children>
</Style>
<!-- Checked Pressed State -->
<Style Selector="^:pressed">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPressed}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedPressed}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedPressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedPressed}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedPressed}" />
</Style>
</Style.Children>
</Style>
<!-- Checked Disabled State -->
<Style Selector="^:disabled">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedDisabled}" />
</Style>
</Style.Children>
</Style>
</Style.Children>
<Style Selector="^:indeterminate">
<!-- Indeterminate Normal State -->
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminate}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminate}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminate}" />
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminate}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminate}" />
</Style>
<Style Selector="^:indeterminate">
<!-- Indeterminate Normal State -->
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminate}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminate}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminate}" />
<Style.Children>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminate}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminate}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminate}" />
<Setter Property="Data" Value="M1536 1536v-1024h-1024v1024h1024z" />
<Setter Property="Width" Value="7" />
<Setter Property="Opacity" Value="1" />
</Style>
<!-- Indeterminate PointerOver State -->
<Style Selector="^:pointerover">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePointerOver}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminatePointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminatePointerOver}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminatePointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminatePointerOver}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminatePointerOver}" />
</Style>
</Style.Children>
</Style>
<!-- Indeterminate Pressed State -->
<Style Selector="^:pressed">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePressed}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminatePressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminatePressed}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminatePressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminatePressed}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminatePressed}" />
</Style>
</Style.Children>
</Style>
<!-- Indeterminate Disabled State -->
<Style Selector="^:disabled">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminateDisabled}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminateDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminateDisabled}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminateDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminateDisabled}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminateDisabled}" />
</Style>
</Style.Children>
</Style>
</Style.Children>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminate}" />
<Setter Property="Data" Value="M1536 1536v-1024h-1024v1024h1024z" />
<Setter Property="Width" Value="7" />
<Setter Property="Opacity" Value="1" />
</Style>
<!-- Indeterminate PointerOver State -->
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePointerOver}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminatePointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminatePointerOver}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminatePointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminatePointerOver}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminatePointerOver}" />
</Style>
</Style>
<!-- Indeterminate Pressed State -->
<Style Selector="^:pressed">
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePressed}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminatePressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminatePressed}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminatePressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminatePressed}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminatePressed}" />
</Style>
</Style>
<!-- Indeterminate Disabled State -->
<Style Selector="^:disabled">
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminateDisabled}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminateDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminateDisabled}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminateDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminateDisabled}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminateDisabled}" />
</Style>
</Style>
</Style.Children>
</Style>
</Style>

10
src/Avalonia.X11/X11Platform.cs

@ -5,6 +5,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Dialogs;
using Avalonia.FreeDesktop;
using Avalonia.FreeDesktop.DBusIme;
using Avalonia.Input;
@ -15,7 +16,6 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.X11;
using Avalonia.X11.Glx;
using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.XLib;
namespace Avalonia.X11
@ -80,7 +80,7 @@ namespace Avalonia.X11
.Bind<IClipboard>().ToConstant(new X11Clipboard(this))
.Bind<IPlatformSettings>().ToConstant(new PlatformSettingsStub())
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader(Info))
.Bind<ISystemDialogImpl>().ToConstant(new GtkSystemDialog())
.Bind<ISystemDialogImpl>().ToConstant(DBusSystemDialog.TryCreate() as ISystemDialogImpl ?? new ManagedFileDialogExtensions.ManagedSystemDialogImpl<Window>())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this));
@ -209,10 +209,10 @@ namespace Avalonia
public bool OverlayPopups { get; set; }
/// <summary>
/// Enables global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc).
/// The default value is false.
/// Enables native file dialogs as well as global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc).
/// The default value is true.
/// </summary>
public bool UseDBusMenu { get; set; }
public bool UseDBusMenu { get; set; } = true;
/// <summary>
/// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.

2
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -16,7 +16,7 @@ namespace Avalonia.Skia
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel;
var bidiLevel = options.BidiLevel;
var culture = options.Culture;
using (var buffer = new Buffer())

57
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor

@ -1,19 +1,52 @@
<div id="container" class="avalonia-container" tabindex="0" oncontextmenu="return false;"
ontouchstart="@OnTouchStart"
ontouchend="@OnTouchEnd"
ontouchcancel="@OnTouchCancel"
ontouchmove="@OnTouchMove"
onmousemove="@OnMouseMove"
onmousedown="@OnMouseDown"
onmouseup="@OnMouseUp"
onwheel="@OnWheel"
onkeydown="@OnKeyDown"
onkeyup="@OnKeyUp">
onkeyup="@OnKeyUp"
onpointerdown="@OnPointerDown"
onpointerup="@OnPointerUp"
onpointermove="@OnPointerMove">
<canvas @ref="_htmlCanvas" @attributes="AdditionalAttributes"/>
<input @ref="_inputElement"
class="overlay"
type="text"
oninput="@OnInput"/>
<canvas id="htmlCanvas" @ref="_htmlCanvas" @attributes="AdditionalAttributes"/>
<div id="nativeControlsContainer" @ref="_nativeControlsContainer" />
<input id="inputElement" @ref="_inputElement" type="text" oninput="@OnInput"
onpaste="return false;"
oncopy="return false;"
oncut="return false;"/>
</div>
<style>
#htmlCanvas {
opacity: 1;
background-color: #ccc;
position: fixed;
width: 100vw;
height: 100vh;
top: 0px;
left: 0px;
z-index: 500;
}
#nativeControlsContainer {
position: fixed;
width: 100vw;
height: 100vh;
top: 0px;
left: 0px;
z-index: 700;
}
#inputElement {
opacity: 0.0;
position: fixed;
width: 100vw;
height: 100vh;
top: 0px;
left: 0px;
z-index: 1000;
}
</style>

146
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs

@ -1,5 +1,6 @@
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Embedding;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
@ -18,14 +19,16 @@ namespace Avalonia.Web.Blazor
private EmbeddableControlRoot _topLevel;
// Interop
private SKHtmlCanvasInterop _interop = null!;
private SizeWatcherInterop _sizeWatcher = null!;
private DpiWatcherInterop _dpiWatcher = null!;
private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null!;
private InputHelperInterop _inputHelper = null!;
private InputHelperInterop _canvasHelper = null!;
private SKHtmlCanvasInterop? _interop = null;
private SizeWatcherInterop? _sizeWatcher = null;
private DpiWatcherInterop? _dpiWatcher = null;
private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null;
private InputHelperInterop? _inputHelper = null;
private InputHelperInterop? _canvasHelper = null;
private NativeControlHostInterop? _nativeControlHost = null;
private ElementReference _htmlCanvas;
private ElementReference _inputElement;
private ElementReference _nativeControlsContainer;
private double _dpi = 1;
private SKSize _canvasSize = new (100, 100);
@ -49,24 +52,11 @@ namespace Avalonia.Web.Blazor
}
}
private void OnTouchStart(TouchEventArgs e)
internal INativeControlHostImpl GetNativeControlHostImpl()
{
foreach (var touch in e.ChangedTouches)
{
_topLevelImpl.RawTouchEvent(RawPointerEventType.TouchBegin, new Point(touch.ClientX, touch.ClientY),
GetModifiers(e), touch.Identifier);
}
}
private void OnTouchEnd(TouchEventArgs e)
{
foreach (var touch in e.ChangedTouches)
{
_topLevelImpl.RawTouchEvent(RawPointerEventType.TouchEnd, new Point(touch.ClientX, touch.ClientY),
GetModifiers(e), touch.Identifier);
}
return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet");
}
private void OnTouchCancel(TouchEventArgs e)
{
foreach (var touch in e.ChangedTouches)
@ -85,53 +75,72 @@ namespace Avalonia.Web.Blazor
}
}
private void OnMouseMove(MouseEventArgs e)
private void OnPointerMove(Microsoft.AspNetCore.Components.Web.PointerEventArgs e)
{
_topLevelImpl.RawMouseEvent(RawPointerEventType.Move, new Point(e.ClientX, e.ClientY), GetModifiers(e));
if (e.PointerType != "touch")
{
_topLevelImpl.RawMouseEvent(RawPointerEventType.Move, new Point(e.ClientX, e.ClientY), GetModifiers(e));
}
}
private void OnMouseUp(MouseEventArgs e)
private void OnPointerUp(Microsoft.AspNetCore.Components.Web.PointerEventArgs e)
{
RawPointerEventType type = default;
switch (e.Button)
if (e.PointerType == "touch")
{
_topLevelImpl.RawTouchEvent(RawPointerEventType.TouchEnd, new Point(e.ClientX, e.ClientY),
GetModifiers(e), e.PointerId);
}
else
{
case 0:
type = RawPointerEventType.LeftButtonUp;
break;
RawPointerEventType type = default;
case 1:
type = RawPointerEventType.MiddleButtonUp;
break;
switch (e.Button)
{
case 0:
type = RawPointerEventType.LeftButtonUp;
break;
case 2:
type = RawPointerEventType.RightButtonUp;
break;
}
case 1:
type = RawPointerEventType.MiddleButtonUp;
break;
case 2:
type = RawPointerEventType.RightButtonUp;
break;
}
_topLevelImpl.RawMouseEvent(type, new Point(e.ClientX, e.ClientY), GetModifiers(e));
_topLevelImpl.RawMouseEvent(type, new Point(e.ClientX, e.ClientY), GetModifiers(e));
}
}
private void OnMouseDown(MouseEventArgs e)
private void OnPointerDown(Microsoft.AspNetCore.Components.Web.PointerEventArgs e)
{
RawPointerEventType type = default;
switch (e.Button)
if (e.PointerType == "touch")
{
_topLevelImpl.RawTouchEvent(RawPointerEventType.TouchBegin, new Point(e.ClientX, e.ClientY),
GetModifiers(e), e.PointerId);
}
else
{
case 0:
type = RawPointerEventType.LeftButtonDown;
break;
RawPointerEventType type = default;
case 1:
type = RawPointerEventType.MiddleButtonDown;
break;
switch (e.Button)
{
case 0:
type = RawPointerEventType.LeftButtonDown;
break;
case 2:
type = RawPointerEventType.RightButtonDown;
break;
}
case 1:
type = RawPointerEventType.MiddleButtonDown;
break;
_topLevelImpl.RawMouseEvent(type, new Point(e.ClientX, e.ClientY), GetModifiers(e));
case 2:
type = RawPointerEventType.RightButtonDown;
break;
}
_topLevelImpl.RawMouseEvent(type, new Point(e.ClientX, e.ClientY), GetModifiers(e));
}
}
private void OnWheel(WheelEventArgs e)
@ -181,7 +190,7 @@ namespace Avalonia.Web.Blazor
return modifiers;
}
private static RawInputModifiers GetModifiers(MouseEventArgs e)
private static RawInputModifiers GetModifiers(Microsoft.AspNetCore.Components.Web.PointerEventArgs e)
{
var modifiers = RawInputModifiers.None;
@ -224,12 +233,12 @@ namespace Avalonia.Web.Blazor
private void OnKeyDown(KeyboardEventArgs e)
{
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Code, GetModifiers(e));
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Code, e.Key, GetModifiers(e));
}
private void OnKeyUp(KeyboardEventArgs e)
{
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, GetModifiers(e));
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, e.Key, GetModifiers(e));
}
private void OnInput(ChangeEventArgs e)
@ -243,7 +252,7 @@ namespace Avalonia.Web.Blazor
}
}
_inputHelper.Clear();
_inputHelper?.Clear();
}
[Parameter(CaptureUnmatchedValues = true)]
@ -253,6 +262,8 @@ namespace Avalonia.Web.Blazor
{
if (firstRender)
{
AvaloniaLocator.CurrentMutable.Bind<IJSInProcessRuntime>().ToConstant((IJSInProcessRuntime)Js);
_inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement);
_canvasHelper = await InputHelperInterop.ImportAsync(Js, _htmlCanvas);
@ -264,6 +275,8 @@ namespace Avalonia.Web.Blazor
_canvasHelper.SetCursor(x); //windows
};
_nativeControlHost = await NativeControlHostInterop.ImportAsync(Js, _nativeControlsContainer);
Console.WriteLine("starting html canvas setup");
_interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame);
@ -319,9 +332,9 @@ namespace Avalonia.Web.Blazor
public void Dispose()
{
_dpiWatcher.Unsubscribe(OnDpiChanged);
_sizeWatcher.Dispose();
_interop.Dispose();
_dpiWatcher?.Unsubscribe(OnDpiChanged);
_sizeWatcher?.Dispose();
_interop?.Dispose();
}
private void ForceBlit()
@ -345,7 +358,7 @@ namespace Avalonia.Web.Blazor
{
_dpi = newDpi;
_interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
@ -359,7 +372,7 @@ namespace Avalonia.Web.Blazor
{
_canvasSize = newSize;
_interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
@ -369,6 +382,11 @@ namespace Avalonia.Web.Blazor
public void SetClient(ITextInputMethodClient? client)
{
if (_inputHelper is null)
{
return;
}
_inputHelper.Clear();
var active = client is { };
@ -394,7 +412,7 @@ namespace Avalonia.Web.Blazor
public void Reset()
{
_inputHelper.Clear();
_inputHelper?.Clear();
}
}
}

34
src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor
{
internal class ClipboardImpl : IClipboard
{
public async Task<string> GetTextAsync()
{
return await AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>().
InvokeAsync<string>("navigator.clipboard.readText");
}
public async Task SetTextAsync(string text)
{
await AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>().
InvokeAsync<string>("navigator.clipboard.writeText",text);
}
public async Task ClearAsync() => await SetTextAsync("");
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object> GetDataAsync(string format) => Task.FromResult<object>(new());
}
}

152
src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs

@ -0,0 +1,152 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class NativeControlHostInterop : JSModuleInterop, INativeControlHostImpl
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/NativeControlHost.js";
private const string CreateDefaultChildSymbol = "NativeControlHost.CreateDefaultChild";
private const string CreateAttachmentSymbol = "NativeControlHost.CreateAttachment";
private const string GetReferenceSymbol = "NativeControlHost.GetReference";
private readonly ElementReference hostElement;
public static async Task<NativeControlHostInterop> ImportAsync(IJSRuntime js, ElementReference element)
{
var interop = new NativeControlHostInterop(js, element);
await interop.ImportAsync();
return interop;
}
public NativeControlHostInterop(IJSRuntime js, ElementReference element)
: base(js, JsFilename)
{
hostElement = element;
}
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent)
{
var element = Invoke<IJSInProcessObjectReference>(CreateDefaultChildSymbol);
return new JSObjectControlHandle(element);
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create)
{
Attachment? a = null;
try
{
using var hostElementJsReference = Invoke<IJSInProcessObjectReference>(GetReferenceSymbol, hostElement);
var child = create(new JSObjectControlHandle(hostElementJsReference));
var attachmenetReference = Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
// It has to be assigned to the variable before property setter is called so we dispose it on exception
#pragma warning disable IDE0017 // Simplify object initialization
a = new Attachment(attachmenetReference, child);
#pragma warning restore IDE0017 // Simplify object initialization
a.AttachedTo = this;
return a;
}
catch
{
a?.Dispose();
throw;
}
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle)
{
var attachmenetReference = Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
var a = new Attachment(attachmenetReference, handle);
a.AttachedTo = this;
return a;
}
public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle;
class Attachment : INativeControlHostControlTopLevelAttachment
{
private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle";
private const string AttachToSymbol = "AttachTo";
private const string ShowInBoundsSymbol = "ShowInBounds";
private const string HideWithSizeSymbol = "HideWithSize";
private const string ReleaseChildSymbol = "ReleaseChild";
private IJSInProcessObjectReference? _native;
private NativeControlHostInterop? _attachedTo;
public Attachment(IJSInProcessObjectReference native, IPlatformHandle handle)
{
_native = native;
_native.InvokeVoid(InitializeWithChildHandleSymbol, ((JSObjectControlHandle)handle).Object);
}
public void Dispose()
{
if (_native != null)
{
_native.InvokeVoid(ReleaseChildSymbol);
_native.Dispose();
_native = null;
}
}
public INativeControlHostImpl? AttachedTo
{
get => _attachedTo!;
set
{
CheckDisposed();
var host = (NativeControlHostInterop?)value;
if (host == null)
{
_native.InvokeVoid(AttachToSymbol);
}
else
{
_native.InvokeVoid(AttachToSymbol, host.hostElement);
}
_attachedTo = host;
}
}
public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostInterop;
public void HideWithSize(Size size)
{
CheckDisposed();
if (_attachedTo == null)
return;
_native.InvokeVoid(HideWithSizeSymbol, Math.Max(1, (float)size.Width), Math.Max(1, (float)size.Height));
}
public void ShowInBounds(Rect bounds)
{
CheckDisposed();
if (_attachedTo == null)
throw new InvalidOperationException("Native control isn't attached to a toplevel");
bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width),
Math.Max(1, bounds.Height));
_native.InvokeVoid(ShowInBoundsSymbol, (float)bounds.X, (float)bounds.Y, (float)bounds.Width, (float)bounds.Height);
}
[MemberNotNull(nameof(_native))]
private void CheckDisposed()
{
if (_native == null)
throw new ObjectDisposedException(nameof(Attachment));
}
}
}
}

56
src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts

@ -0,0 +1,56 @@
export class NativeControlHost {
public static CreateDefaultChild(parent: HTMLElement): HTMLElement {
return document.createElement("div");
}
// Used to convert ElementReference to JSObjectReference.
// Is there a better way?
public static GetReference(element: Element): Element {
return element;
}
public static CreateAttachment(): NativeControlHostTopLevelAttachment {
return new NativeControlHostTopLevelAttachment();
}
}
class NativeControlHostTopLevelAttachment
{
_child: HTMLElement;
_host: HTMLElement;
InitializeWithChildHandle(child: HTMLElement) {
this._child = child;
this._child.style.position = "absolute";
}
AttachTo(host: HTMLElement): void {
if (this._host) {
this._host.removeChild(this._child);
}
this._host = host;
if (this._host) {
this._host.appendChild(this._child);
}
}
ShowInBounds(x: number, y: number, width: number, height: number): void {
this._child.style.top = y + "px";
this._child.style.left = x + "px";
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "block";
}
HideWithSize(width: number, height: number): void {
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "none";
}
ReleaseChild(): void {
this._child = null;
}
}

35
src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs

@ -0,0 +1,35 @@
#nullable enable
using Avalonia.Controls.Platform;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor
{
public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle
{
internal const string ElementReferenceDescriptor = "JSObjectReference";
public JSObjectControlHandle(IJSObjectReference reference)
{
Object = reference;
}
public IJSObjectReference Object { get; }
public IntPtr Handle => throw new NotSupportedException();
public string? HandleDescriptor => ElementReferenceDescriptor;
public void Destroy()
{
if (Object is IJSInProcessObjectReference inProcess)
{
inProcess.Dispose();
}
else
{
_ = Object.DisposeAsync();
}
}
}
}

23
src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs

@ -13,19 +13,19 @@ using SkiaSharp;
namespace Avalonia.Web.Blazor
{
internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod
internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost
{
private Size _clientSize;
private BlazorSkiaSurface? _currentSurface;
private IInputRoot? _inputRoot;
private readonly Stopwatch _sw = Stopwatch.StartNew();
private readonly ITextInputMethodImpl _textInputMethod;
private readonly AvaloniaView _avaloniaView;
private readonly TouchDevice _touchDevice;
private string _currentCursor = CssCursor.Default;
public RazorViewTopLevelImpl(ITextInputMethodImpl textInputMethod)
public RazorViewTopLevelImpl(AvaloniaView avaloniaView)
{
_textInputMethod = textInputMethod;
_avaloniaView = avaloniaView;
TransparencyLevel = WindowTransparencyLevel.None;
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
_touchDevice = new TouchDevice();
@ -91,9 +91,16 @@ namespace Avalonia.Web.Blazor
}
}
public void RawKeyboardEvent(RawKeyEventType type, string key, RawInputModifiers modifiers)
public void RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers)
{
if (Keycodes.KeyCodes.TryGetValue(key, out var avkey))
if (Keycodes.KeyCodes.TryGetValue(code, out var avkey))
{
if (_inputRoot is { })
{
Input?.Invoke(new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers));
}
}
else if (Keycodes.KeyCodes.TryGetValue(key, out avkey))
{
if (_inputRoot is { })
{
@ -175,6 +182,8 @@ namespace Avalonia.Web.Blazor
public WindowTransparencyLevel TransparencyLevel { get; }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; }
public ITextInputMethodImpl TextInputMethod => _textInputMethod;
public ITextInputMethodImpl TextInputMethod => _avaloniaView;
public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl();
}
}

15
src/Web/Avalonia.Web.Blazor/WinStubs.cs

@ -8,21 +8,6 @@ using Avalonia.Platform;
namespace Avalonia.Web.Blazor
{
internal class ClipboardStub : IClipboard
{
public Task<string> GetTextAsync() => Task.FromResult("");
public Task SetTextAsync(string text) => Task.CompletedTask;
public Task ClearAsync() => Task.CompletedTask;
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object> GetDataAsync(string format) => Task.FromResult<object>(new ());
}
internal class IconLoaderStub : IPlatformIconLoader
{
private class IconStub : IWindowIconImpl

2
src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs

@ -34,7 +34,7 @@ namespace Avalonia.Web.Blazor
var instance = new BlazorWindowingPlatform();
s_keyboard = new KeyboardDevice();
AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToSingleton<ClipboardStub>()
.Bind<IClipboard>().ToSingleton<ClipboardImpl>()
.Bind<ICursorFactory>().ToSingleton<CssCursorFactory>()
.Bind<IKeyboardDevice>().ToConstant(s_keyboard)
.Bind<IPlatformSettings>().ToConstant(instance)

2
src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

@ -16,7 +16,7 @@ namespace Avalonia.Direct2D1.Media
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel;
var bidiLevel = options.BidiLevel;
var culture = options.Culture;
using (var buffer = new Buffer())

4
src/Windows/Avalonia.Win32/Win32NativeControlHost.cs

@ -94,6 +94,10 @@ namespace Avalonia.Win32
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero);
if (Handle == IntPtr.Zero)
throw new InvalidOperationException("Unable to create child window for native control host. Application manifest with supported OS list might be required.");
if (layered)
UnmanagedMethods.SetLayeredWindowAttributes(Handle, 0, 255,
UnmanagedMethods.LayeredWindowFlags.LWA_ALPHA);

4
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -43,7 +43,7 @@ namespace Avalonia.iOS
MultipleTouchEnabled = true;
}
internal class TopLevelImpl : ITopLevelImplWithTextInputMethod
internal class TopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost
{
private readonly AvaloniaView _view;
public AvaloniaView View => _view;
@ -51,6 +51,7 @@ namespace Avalonia.iOS
public TopLevelImpl(AvaloniaView view)
{
_view = view;
NativeControlHost = new NativeControlHostImpl(_view);
}
public void Dispose()
@ -112,6 +113,7 @@ namespace Avalonia.iOS
new AcrylicPlatformCompensationLevels();
public ITextInputMethodImpl? TextInputMethod => _view;
public INativeControlHostImpl NativeControlHost { get; }
}
[Export("layerClass")]

160
src/iOS/Avalonia.iOS/NativeControlHostImpl.cs

@ -0,0 +1,160 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using CoreGraphics;
using ObjCRuntime;
using UIKit;
namespace Avalonia.iOS
{
internal class NativeControlHostImpl : INativeControlHostImpl
{
private readonly AvaloniaView _avaloniaView;
public NativeControlHostImpl(AvaloniaView avaloniaView)
{
_avaloniaView = avaloniaView;
}
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent)
{
return new UIViewControlHandle(new UIView());
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create)
{
var parent = new UIViewControlHandle(_avaloniaView);
NativeControlAttachment? attachment = null;
try
{
var child = create(parent);
// It has to be assigned to the variable before property setter is called so we dispose it on exception
#pragma warning disable IDE0017 // Simplify object initialization
attachment = new NativeControlAttachment(child);
#pragma warning restore IDE0017 // Simplify object initialization
attachment.AttachedTo = this;
return attachment;
}
catch
{
attachment?.Dispose();
throw;
}
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle)
{
return new NativeControlAttachment(handle)
{
AttachedTo = this
};
}
public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == UIViewControlHandle.UIViewDescriptor;
private class ViewHolder : UIView
{
public ViewHolder(IntPtr handle) : base(new NativeHandle(handle))
{
}
}
private class NativeControlAttachment : INativeControlHostControlTopLevelAttachment
{
// ReSharper disable once NotAccessedField.Local (keep GC reference)
private IPlatformHandle? _child;
private UIView? _view;
private NativeControlHostImpl? _attachedTo;
public NativeControlAttachment(IPlatformHandle child)
{
_child = child;
_view = (child as UIViewControlHandle)?.View ?? new ViewHolder(child.Handle);
}
[MemberNotNull(nameof(_view))]
private void CheckDisposed()
{
if (_view == null)
throw new ObjectDisposedException(nameof(NativeControlAttachment));
}
public void Dispose()
{
_view?.RemoveFromSuperview();
_child = null;
_attachedTo = null;
_view?.Dispose();
_view = null;
}
public INativeControlHostImpl? AttachedTo
{
get => _attachedTo;
set
{
CheckDisposed();
_attachedTo = (NativeControlHostImpl?)value;
if (_attachedTo == null)
{
_view.RemoveFromSuperview();
}
else
{
_attachedTo._avaloniaView.AddSubview(_view);
}
}
}
public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostImpl;
public void HideWithSize(Size size)
{
CheckDisposed();
if (_attachedTo == null)
return;
_view.Hidden = true;
_view.Frame = new CGRect(0d, 0d, Math.Max(1d, size.Width), Math.Max(1d, size.Height));
}
public void ShowInBounds(Rect bounds)
{
CheckDisposed();
if (_attachedTo == null)
throw new InvalidOperationException("The control isn't currently attached to a toplevel");
_view.Frame = new CGRect(bounds.X, bounds.Y, Math.Max(1d, bounds.Width), Math.Max(1d, bounds.Height));
_view.Hidden = false;
}
}
}
public class UIViewControlHandle : INativeControlHostDestroyableControlHandle
{
internal const string UIViewDescriptor = "UIView";
public UIViewControlHandle(UIView view)
{
View = view;
}
public UIView View { get; }
public string HandleDescriptor => UIViewDescriptor;
IntPtr IPlatformHandle.Handle => View.Handle.Handle;
public void Destroy()
{
View.Dispose();
}
}
}

21
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -446,6 +446,27 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Should_Reset_Popup_Parent_On_Target_Detached()
{
using (Application())
{
var userControl = new UserControl();
var window = PreparedWindow(userControl);
window.Show();
var menu = new ContextMenu();
userControl.ContextMenu = menu;
menu.Open();
var popup = Assert.IsType<Popup>(menu.Parent);
Assert.NotNull(popup.Parent);
window.Content = null;
Assert.Null(popup.Parent);
}
}
[Fact]
public void Context_Menu_In_Resources_Can_Be_Shared()
{

47
tests/Avalonia.Controls.UnitTests/FlyoutTests.cs

@ -432,6 +432,48 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Should_Reset_Popup_Parent_On_Target_Detached()
{
using (CreateServicesWithFocus())
{
var userControl = new UserControl();
var window = PreparedWindow(userControl);
window.Show();
var flyout = new TestFlyout();
flyout.ShowAt(userControl);
var popup = Assert.IsType<Popup>(flyout.Popup);
Assert.NotNull(popup.Parent);
window.Content = null;
Assert.Null(popup.Parent);
}
}
[Fact]
public void Should_Reset_Popup_Parent_On_Target_Attach_Following_Detach()
{
using (CreateServicesWithFocus())
{
var userControl = new UserControl();
var window = PreparedWindow(userControl);
window.Show();
var flyout = new TestFlyout();
flyout.ShowAt(userControl);
var popup = Assert.IsType<Popup>(flyout.Popup);
Assert.NotNull(popup.Parent);
flyout.Hide();
flyout.ShowAt(userControl);
Assert.NotNull(popup.Parent);
}
}
[Fact]
public void ContextFlyout_Can_Be_Set_In_Styles()
{
@ -549,5 +591,10 @@ namespace Avalonia.Controls.UnitTests
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
KeyModifiers.None);
}
public class TestFlyout : Flyout
{
public new Popup Popup => base.Popup;
}
}
}

26
tests/Avalonia.Controls.UnitTests/ViewboxTests.cs

@ -181,6 +181,32 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(child.GetLogicalParent());
}
[Fact]
public void Changing_Child_Should_Invalidate_Layout()
{
var target = new Viewbox();
target.Child = new Canvas
{
Width = 100,
Height = 100,
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(100, 100), target.DesiredSize);
target.Child = new Canvas
{
Width = 200,
Height = 200,
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(200, 200), target.DesiredSize);
}
private bool TryGetScale(Viewbox viewbox, out Vector scale)
{
if (viewbox.InternalTransform is null)

32
tests/Avalonia.Markup.UnitTests/Data/BindingTests_Method.cs

@ -0,0 +1,32 @@
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data
{
public class BindingTests_Method
{
[Fact]
public void Binding_To_Private_Methods_Shouldnt_Work()
{
var vm = new TestClass();
var target = new Button
{
DataContext = vm,
[!Button.CommandProperty] = new Binding("MyMethod"),
};
target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
Assert.False(vm.IsSet);
}
class TestClass
{
public bool IsSet { get; set; }
private void MyMethod() => IsSet = true;
}
}
}

8
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -628,11 +628,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border'>
<Style.Children>
<Style Selector='^.foo'>
<Setter Property='Background' Value='Red'/>
</Style>
</Style.Children>
<Style Selector='^.foo'>
<Setter Property='Background' Value='Red'/>
</Style>
</Style>
</Window.Styles>
<StackPanel>

184
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -543,6 +543,98 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Fact]
public void Should_Get_Distance_From_CharacterHit_Mixed_TextBuffer()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new MixedTextBufferTextSource();
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(10));
Assert.Equal(72.01171875, distance);
distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(20));
Assert.Equal(144.0234375, distance);
distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(30));
Assert.Equal(216.03515625, distance);
distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(40));
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, distance);
}
}
[Fact]
public void Should_Get_TextBounds_From_Mixed_TextBuffer()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new MixedTextBufferTextSource();
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var textBounds = textLine.GetTextBounds(0, 10);
Assert.Equal(1, textBounds.Count);
Assert.Equal(72.01171875, textBounds[0].Rectangle.Width);
textBounds = textLine.GetTextBounds(0, 20);
Assert.Equal(1, textBounds.Count);
Assert.Equal(144.0234375, textBounds[0].Rectangle.Width);
textBounds = textLine.GetTextBounds(0, 30);
Assert.Equal(1, textBounds.Count);
Assert.Equal(216.03515625, textBounds[0].Rectangle.Width);
textBounds = textLine.GetTextBounds(0, 40);
Assert.Equal(1, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds[0].Rectangle.Width);
}
}
private class MixedTextBufferTextSource : ITextSource
{
public TextRun? GetTextRun(int textSourceIndex)
{
switch (textSourceIndex)
{
case 0:
return new TextCharacters(new ReadOnlySlice<char>("aaaaaaaaaa".AsMemory()), new GenericTextRunProperties(Typeface.Default));
case 10:
return new TextCharacters(new ReadOnlySlice<char>("bbbbbbbbbb".AsMemory()), new GenericTextRunProperties(Typeface.Default));
case 20:
return new TextCharacters(new ReadOnlySlice<char>("cccccccccc".AsMemory()), new GenericTextRunProperties(Typeface.Default));
case 30:
return new TextCharacters(new ReadOnlySlice<char>("dddddddddd".AsMemory()), new GenericTextRunProperties(Typeface.Default));
default:
return null;
}
}
}
private class DrawableRunTextSource : ITextSource
{
const string Text = "_A_A";
@ -713,35 +805,95 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
[Fact]
public void Should_Get_TextBounds_BiDi()
public void Should_Get_TextBounds_BiDi_LeftToRight()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var text = "0123".AsMemory();
var ltrOptions = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture);
var rtlOptions = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 1, CultureInfo.CurrentCulture);
var text = "אאא AAA";
var textSource = new SingleBufferTextSource(text, defaultProperties);
var textRuns = new List<TextRun>
{
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text), ltrOptions), defaultProperties),
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length, text.Length), ltrOptions), defaultProperties),
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 2, text.Length), rtlOptions), defaultProperties),
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 3, text.Length), ltrOptions), defaultProperties)
};
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, 200,
new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0));
var textSource = new FixedRunsTextSource(textRuns);
var textBounds = textLine.GetTextBounds(0, 3);
var firstRun = textLine.TextRuns[0] as ShapedTextCharacters;
Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(3, 4);
var secondRun = textLine.TextRuns[1] as ShapedTextCharacters;
Assert.Equal(1, textBounds.Count);
Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 4);
Assert.Equal(2, textBounds.Count);
Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
Assert.Equal(7.201171875, textBounds[1].Rectangle.Width);
Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left);
textBounds = textLine.GetTextBounds(0, text.Length);
Assert.Equal(2, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
}
}
[Fact]
public void Should_Get_TextBounds_BiDi_RightToLeft()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var text = "אאא AAA";
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
formatter.FormatLine(textSource, 0, 200,
new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0));
var textBounds = textLine.GetTextBounds(0, 4);
var firstRun = textLine.TextRuns[1] as ShapedTextCharacters;
Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(4, 3);
var secondRun = textLine.TextRuns[0] as ShapedTextCharacters;
Assert.Equal(1, textBounds.Count);
Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x=> x.Length));
Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 5);
Assert.Equal(2, textBounds.Count);
Assert.Equal(5, textBounds.Sum(x=> x.TextRunBounds.Sum(x => x.Length)));
Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
Assert.Equal(7.201171875, textBounds[1].Rectangle.Width);
Assert.Equal(textLine.Start + 7.201171875, textBounds[1].Rectangle.Right);
var textBounds = textLine.GetTextBounds(0, text.Length * 4);
textBounds = textLine.GetTextBounds(0, text.Length);
Assert.Equal(3, textBounds.Count);
Assert.Equal(2, textBounds.Count);
Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length)));
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
}
}

2
tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs

@ -15,7 +15,7 @@ namespace Avalonia.UnitTests
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel;
var bidiLevel = options.BidiLevel;
var culture = options.Culture;
using (var buffer = new Buffer())

2
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@ -11,7 +11,7 @@ namespace Avalonia.UnitTests
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel;
var bidiLevel = options.BidiLevel;
var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);

Loading…
Cancel
Save