diff --git a/Avalonia.sln b/Avalonia.sln index 04aad99211..2927ff86df 100644 --- a/Avalonia.sln +++ b/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} diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 1ee4aa56a2..d54cffba08 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/native/Avalonia.Native/src/OSX/AvnPanelWindow.mm b/native/Avalonia.Native/src/OSX/AvnPanelWindow.mm index 2365189010..b49005de8a 100644 --- a/native/Avalonia.Native/src/OSX/AvnPanelWindow.mm +++ b/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" diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 02526afbcb..5436ad22f3 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/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 \ No newline at end of file +@end diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index f51c693777..9fc7ec4fec 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/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(_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 parent = _parent; _parent = NULL; - [self restoreParentWindow]; + + auto window = dynamic_cast(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(_parent.getRaw()); + + if(parent != nullptr) { - if (![uch conformsToProtocol:@protocol(AvnWindowProtocol)]) - { - continue; - } - - id ch = (id ) 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(_parent.operator->()); @@ -293,7 +294,7 @@ } } -- (void)windowDidDeminiaturize:(NSNotification *)notification +- (void)windowDidDeminiaturize:(NSNotification *_Nonnull)notification { auto parent = dynamic_cast(_parent.operator->()); @@ -303,7 +304,7 @@ } } -- (void)windowDidResize:(NSNotification *)notification +- (void)windowDidResize:(NSNotification *_Nonnull)notification { auto parent = dynamic_cast(_parent.operator->()); @@ -313,7 +314,7 @@ } } -- (void)windowWillExitFullScreen:(NSNotification *)notification +- (void)windowWillExitFullScreen:(NSNotification *_Nonnull)notification { auto parent = dynamic_cast(_parent.operator->()); @@ -323,7 +324,7 @@ } } -- (void)windowDidExitFullScreen:(NSNotification *)notification +- (void)windowDidExitFullScreen:(NSNotification *_Nonnull)notification { auto parent = dynamic_cast(_parent.operator->()); @@ -346,7 +347,7 @@ } } -- (void)windowWillEnterFullScreen:(NSNotification *)notification +- (void)windowWillEnterFullScreen:(NSNotification *_Nonnull)notification { auto parent = dynamic_cast(_parent.operator->()); @@ -356,7 +357,7 @@ } } -- (void)windowDidEnterFullScreen:(NSNotification *)notification +- (void)windowDidEnterFullScreen:(NSNotification *_Nonnull)notification { auto parent = dynamic_cast(_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([event timestamp] * 1000), AvnInputModifiersNone, point, delta); } + + _parent->BringToFront(); } - break; + break; case NSEventTypeMouseEntered: { diff --git a/native/Avalonia.Native/src/OSX/INSWindowHolder.h b/native/Avalonia.Native/src/OSX/INSWindowHolder.h index ae64a53e7d..3c5010966b 100644 --- a/native/Avalonia.Native/src/OSX/INSWindowHolder.h +++ b/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 diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 83850e780c..040ba39b6d 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/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 GetWindowProtocol (); + + virtual void BringToFront (); protected: virtual NSWindowStyleMask GetStyle(); diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 6dc59ae4d8..c420736b46 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/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 WindowBaseImpl::GetWindowProtocol() { return (id ) Window; } +void WindowBaseImpl::BringToFront() +{ + // do nothing. +} + extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl) { @autoreleasepool diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index db19497b29..627e29c03d 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -8,10 +8,12 @@ #import "WindowBaseImpl.h" #include "IWindowStateChanged.h" +#include 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 _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; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index d96fe717ab..cae1c09513 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/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(); _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(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; } diff --git a/native/Avalonia.Native/src/OSX/WindowProtocol.h b/native/Avalonia.Native/src/OSX/WindowProtocol.h index 0e5c5869e7..cb5f86bdb9 100644 --- a/native/Avalonia.Native/src/OSX/WindowProtocol.h +++ b/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; diff --git a/readme.md b/readme.md index 1cdaf3b8f8..1009e86c29 100644 --- a/readme.md +++ b/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 +## 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). diff --git a/samples/ControlCatalog.Android/EmbedSample.Android.cs b/samples/ControlCatalog.Android/EmbedSample.Android.cs new file mode 100644 index 0000000000..250121fc53 --- /dev/null +++ b/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 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); + } + } +} diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 44290d9816..33ca511340 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/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(); + }); } } } diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 2b45ac1508..0667644643 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -4,6 +4,7 @@ WinExe net6.0 true + true @@ -12,6 +13,16 @@ 7.0.0-* + + + + + + + PreserveNewest + + + @@ -20,6 +31,8 @@ + + @@ -32,6 +45,7 @@ en + app.manifest diff --git a/samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs b/samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs new file mode 100644 index 0000000000..521d3674eb --- /dev/null +++ b/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 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; + } +} diff --git a/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs b/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs new file mode 100644 index 0000000000..456f77a44d --- /dev/null +++ b/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 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; + } +} diff --git a/samples/interop/NativeEmbedSample/nodes-license.md b/samples/ControlCatalog.NetCore/NativeControls/Gtk/nodes-license.md similarity index 100% rename from samples/interop/NativeEmbedSample/nodes-license.md rename to samples/ControlCatalog.NetCore/NativeControls/Gtk/nodes-license.md diff --git a/samples/interop/NativeEmbedSample/nodes.mp4 b/samples/ControlCatalog.NetCore/NativeControls/Gtk/nodes.mp4 similarity index 100% rename from samples/interop/NativeEmbedSample/nodes.mp4 rename to samples/ControlCatalog.NetCore/NativeControls/Gtk/nodes.mp4 diff --git a/samples/ControlCatalog.NetCore/NativeControls/Mac/EmbedSample.Mac.cs b/samples/ControlCatalog.NetCore/NativeControls/Mac/EmbedSample.Mac.cs new file mode 100644 index 0000000000..7967c9c073 --- /dev/null +++ b/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 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); + } +} diff --git a/samples/ControlCatalog.NetCore/NativeControls/Mac/MacHelper.cs b/samples/ControlCatalog.NetCore/NativeControls/Mac/MacHelper.cs new file mode 100644 index 0000000000..5b3bc9abf1 --- /dev/null +++ b/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; + } +} diff --git a/samples/ControlCatalog.NetCore/NativeControls/Win/EmbedSample.Win.cs b/samples/ControlCatalog.NetCore/NativeControls/Win/EmbedSample.Win.cs new file mode 100644 index 0000000000..77982db0ca --- /dev/null +++ b/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 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 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("", 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); + } +} diff --git a/samples/ControlCatalog.NetCore/NativeControls/Win/WinApi.cs b/samples/ControlCatalog.NetCore/NativeControls/Win/WinApi.cs new file mode 100644 index 0000000000..47d368f7a4 --- /dev/null +++ b/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); +} diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 4464413e63..fd080cfc5b 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/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(); diff --git a/samples/ControlCatalog.NetCore/app.manifest b/samples/ControlCatalog.NetCore/app.manifest new file mode 100644 index 0000000000..db90057191 --- /dev/null +++ b/samples/ControlCatalog.NetCore/app.manifest @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog.Web/App.razor.cs b/samples/ControlCatalog.Web/App.razor.cs index a150824ac3..c0b7ddbe1e 100644 --- a/samples/ControlCatalog.Web/App.razor.cs +++ b/samples/ControlCatalog.Web/App.razor.cs @@ -7,6 +7,10 @@ public partial class App protected override void OnParametersSet() { WebAppBuilder.Configure() + .AfterSetup(_ => + { + ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); + }) .SetupWithSingleViewLifetime(); base.OnParametersSet(); diff --git a/samples/ControlCatalog.Web/EmbedSample.Browser.cs b/samples/ControlCatalog.Web/EmbedSample.Browser.cs new file mode 100644 index 0000000000..5fe14409de --- /dev/null +++ b/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 createDefault) + { + var runtime = AvaloniaLocator.Current.GetRequiredService(); + + if (isSecond) + { + var iframe = runtime.Invoke("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("window.createAppButton"); + + return new JSObjectControlHandle(button); + } + } +} diff --git a/samples/ControlCatalog.Web/Shared/MainLayout.razor.css b/samples/ControlCatalog.Web/Shared/MainLayout.razor.css deleted file mode 100644 index 43c355a47a..0000000000 --- a/samples/ControlCatalog.Web/Shared/MainLayout.razor.css +++ /dev/null @@ -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; - } -} diff --git a/samples/ControlCatalog.Web/wwwroot/css/app.css b/samples/ControlCatalog.Web/wwwroot/css/app.css index d2a8dc525c..49ca14e162 100644 --- a/samples/ControlCatalog.Web/wwwroot/css/app.css +++ b/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; -} diff --git a/samples/ControlCatalog.Web/wwwroot/js/app.js b/samples/ControlCatalog.Web/wwwroot/js/app.js index 5f282702bb..29697661a6 100644 --- a/samples/ControlCatalog.Web/wwwroot/js/app.js +++ b/samples/ControlCatalog.Web/wwwroot/js/app.js @@ -1 +1,10 @@ - \ No newline at end of file +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; +} diff --git a/samples/ControlCatalog.iOS/AppDelegate.cs b/samples/ControlCatalog.iOS/AppDelegate.cs index f1c2241003..f8caffed14 100644 --- a/samples/ControlCatalog.iOS/AppDelegate.cs +++ b/samples/ControlCatalog.iOS/AppDelegate.cs @@ -13,6 +13,13 @@ namespace ControlCatalog [Register("AppDelegate")] public partial class AppDelegate : AvaloniaAppDelegate { - + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .AfterSetup(_ => + { + Pages.EmbedSample.Implementation = new EmbedSampleIOS(); + }); + } } } diff --git a/samples/ControlCatalog.iOS/EmbedSample.iOS.cs b/samples/ControlCatalog.iOS/EmbedSample.iOS.cs new file mode 100644 index 0000000000..ad86d2b578 --- /dev/null +++ b/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 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); + } + } +} diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index bce924a3f2..903c849834 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -14,6 +14,9 @@ + + + @@ -32,5 +35,17 @@ + + + MSBuild:Compile + + + + + + %(Filename) + + + diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 59d724db69..d8dc3bad2d 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/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"> + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs b/samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs new file mode 100644 index 0000000000..14310500ab --- /dev/null +++ b/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 + { + 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().Width < 1200; + this.Find("FirstPanel")!.Classes.Set("mobile", isMobile); + this.Find("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 + { + /// Used to specify which control should be displayed as a demo + IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func createDefault); + } +} diff --git a/samples/interop/NativeEmbedSample/App.xaml b/samples/interop/NativeEmbedSample/App.xaml deleted file mode 100644 index e35ade4087..0000000000 --- a/samples/interop/NativeEmbedSample/App.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/samples/interop/NativeEmbedSample/App.xaml.cs b/samples/interop/NativeEmbedSample/App.xaml.cs deleted file mode 100644 index cb17cfc35d..0000000000 --- a/samples/interop/NativeEmbedSample/App.xaml.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/samples/interop/NativeEmbedSample/EmbedSample.cs b/samples/interop/NativeEmbedSample/EmbedSample.cs deleted file mode 100644 index ab9df11e19..0000000000 --- a/samples/interop/NativeEmbedSample/EmbedSample.cs +++ /dev/null @@ -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 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("", 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); - } - } -} diff --git a/samples/interop/NativeEmbedSample/GtkHelper.cs b/samples/interop/NativeEmbedSample/GtkHelper.cs deleted file mode 100644 index e389a51ef5..0000000000 --- a/samples/interop/NativeEmbedSample/GtkHelper.cs +++ /dev/null @@ -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 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; - } - } -} diff --git a/samples/interop/NativeEmbedSample/MacHelper.cs b/samples/interop/NativeEmbedSample/MacHelper.cs deleted file mode 100644 index 74a06a0a0c..0000000000 --- a/samples/interop/NativeEmbedSample/MacHelper.cs +++ /dev/null @@ -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; - } - } - -} diff --git a/samples/interop/NativeEmbedSample/MainWindow.xaml b/samples/interop/NativeEmbedSample/MainWindow.xaml deleted file mode 100644 index f2161a1bea..0000000000 --- a/samples/interop/NativeEmbedSample/MainWindow.xaml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - Text - - - Tooltip - - - - - - - - - Visible - - - - - - - - Visible - - - - - - diff --git a/samples/interop/NativeEmbedSample/MainWindow.xaml.cs b/samples/interop/NativeEmbedSample/MainWindow.xaml.cs deleted file mode 100644 index 4324aa2762..0000000000 --- a/samples/interop/NativeEmbedSample/MainWindow.xaml.cs +++ /dev/null @@ -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 - { - new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" } - } - }.Open((Control)sender); - } - } -} diff --git a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj deleted file mode 100644 index 2f3ea85e46..0000000000 --- a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - Exe - netcoreapp2.0 - true - true - - - - - - - - - - - - Designer - - - - PreserveNewest - - - - - - - - diff --git a/samples/interop/NativeEmbedSample/Program.cs b/samples/interop/NativeEmbedSample/Program.cs deleted file mode 100644 index baa7837667..0000000000 --- a/samples/interop/NativeEmbedSample/Program.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Avalonia; - -namespace NativeEmbedSample -{ - class Program - { - static int Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .With(new AvaloniaNativePlatformOptions() - { - }) - .UsePlatformDetect(); - - } -} diff --git a/samples/interop/NativeEmbedSample/WinApi.cs b/samples/interop/NativeEmbedSample/WinApi.cs deleted file mode 100644 index 8e5bcdf49e..0000000000 --- a/samples/interop/NativeEmbedSample/WinApi.cs +++ /dev/null @@ -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); - } -} diff --git a/src/Android/Avalonia.Android/AndroidViewControlHandle.cs b/src/Android/Avalonia.Android/AndroidViewControlHandle.cs new file mode 100644 index 0000000000..e999d198c6 --- /dev/null +++ b/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(); + } + } +} diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 8177cf1f69..be0aa27393 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/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; diff --git a/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs b/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs new file mode 100644 index 0000000000..4738bd86f9 --- /dev/null +++ b/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 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(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(); + } + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 65a9adc937..10f98609bf 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/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(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(this); _touchHelper = new AndroidTouchEventsHelper(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(); diff --git a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs index 11d50afe93..c4f4362537 100644 --- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs +++ b/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)) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextBounds.cs index a0b51671f0..93edf68348 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextBounds.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextBounds.cs @@ -10,20 +10,27 @@ namespace Avalonia.Media.TextFormatting /// /// Constructing TextBounds object /// - internal TextBounds(Rect bounds, FlowDirection flowDirection) + internal TextBounds(Rect bounds, FlowDirection flowDirection, IList runBounds) { Rectangle = bounds; FlowDirection = flowDirection; + TextRunBounds = runBounds; } /// /// Bounds rectangle /// - public Rect Rectangle { get; } + public Rect Rectangle { get; internal set; } /// /// Text flow direction inside the boundary rectangle /// public FlowDirection FlowDirection { get; } + + /// + /// Get a list of run bounding rectangles + /// + /// Array of text run bounds + public IList TextRunBounds { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 5d5d45db2d..4f7c43a6d1 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/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) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 73ec055bbe..8b5e2cc2ce 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/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 /// 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 GetTextBounds(int firstTextSourceCharacterIndex, int textLength) + private IReadOnlyList GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength) { - if (firstTextSourceCharacterIndex + textLength <= FirstTextSourceIndex) - { - return Array.Empty(); - } + var characterIndex = firstTextSourceIndex + textLength; var result = new List(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 { 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 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(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 { 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 GetTextBounds(int firstTextSourceIndex, int textLength) + { + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) + { + return GetTextBoundsLeftToRight(firstTextSourceIndex, textLength); + } + + return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength); + } + public TextLineImpl FinalizeLine() { _textLineMetrics = CreateLineMetrics(); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs new file mode 100644 index 0000000000..91150160ed --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs @@ -0,0 +1,39 @@ +namespace Avalonia.Media.TextFormatting +{ + /// + /// The bounding rectangle of text run + /// + public sealed class TextRunBounds + { + /// + /// Constructing TextRunBounds + /// + internal TextRunBounds(Rect bounds, int firstCharacterIndex, int length, TextRun textRun) + { + Rectangle = bounds; + TextSourceCharacterIndex = firstCharacterIndex; + Length = length; + TextRun = textRun; + } + + /// + /// First text source character index of text run + /// + public int TextSourceCharacterIndex { get; } + + /// + /// character length of bounded text run + /// + public int Length { get; } + + /// + /// Text run bounding rectangle + /// + public Rect Rectangle { get; } + + /// + /// text run + /// + public TextRun TextRun { get; } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs index a7fe92dc9a..4e75bb921e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs +++ b/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 /// /// Get the bidi level of the text. /// - public sbyte BidLevel { get; } + public sbyte BidiLevel { get; } /// /// Get the culture. diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 8fcf5eec8a..000e588bad 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -94,7 +94,6 @@ namespace Avalonia.Styling /// /// Gets the style's setters. /// - [Content] public IList Setters => _setters ??= new List(); /// @@ -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)); diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 2b122d4174..7b35e35278 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/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(); } } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index d024f86b32..4801fa69f0 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/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); diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index ea9ae7bb0f..3523cd5214 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/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; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index bbe6aeb7ee..1a69d1218c 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/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; diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs index 01a41a0157..aabfd3ef18 100644 --- a/src/Avalonia.Controls/Viewbox.cs +++ b/src/Avalonia.Controls/Viewbox.cs @@ -168,6 +168,8 @@ namespace Avalonia.Controls if (_child is not null) VisualChildren.Add(_child); + + InvalidateMeasure(); } } } diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index a311efdfb0..80159c82d7 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -14,6 +14,10 @@ + + + + diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index 1970c5557d..e4025453c4 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -8,7 +8,7 @@ namespace Avalonia.Dialogs { public static class ManagedFileDialogExtensions { - private class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new() + internal class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new() { async Task Show(SystemDialog d, Window parent, ManagedFileDialogOptions options = null) { @@ -141,7 +141,7 @@ namespace Avalonia.Dialogs public static Task ShowManagedAsync(this OpenFileDialog dialog, Window parent, ManagedFileDialogOptions options = null) => ShowManagedAsync(dialog, parent, options); - + public static Task ShowManagedAsync(this OpenFileDialog dialog, Window parent, ManagedFileDialogOptions options = null) where TWindow : Window, new() { diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index e9d6394aa5..a5cb207223 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -2,10 +2,12 @@ net6.0;netstandard2.0 + enable + diff --git a/src/Avalonia.FreeDesktop/DBusFileChooser.cs b/src/Avalonia.FreeDesktop/DBusFileChooser.cs new file mode 100644 index 0000000000..24db614a02 --- /dev/null +++ b/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 OpenFileAsync(string ParentWindow, string Title, IDictionary Options); + Task SaveFileAsync(string ParentWindow, string Title, IDictionary Options); + Task SaveFilesAsync(string ParentWindow, string Title, IDictionary Options); + Task GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action handler); + } + + [Dictionary] + internal class FileChooserProperties + { + public uint Version { get; set; } + } + + internal static class FileChooserExtensions + { + public static Task GetVersionAsync(this IFileChooser o) => o.GetAsync("version"); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index c14539d7bf..7204e51dbd 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -6,7 +6,7 @@ using Tmds.DBus; namespace Avalonia.FreeDesktop { - public class DBusHelper + public static class DBusHelper { /// /// This class uses synchronous execution at DBus connection establishment stage @@ -14,14 +14,14 @@ namespace Avalonia.FreeDesktop /// 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 { diff --git a/src/Avalonia.FreeDesktop/DBusRequest.cs b/src/Avalonia.FreeDesktop/DBusRequest.cs new file mode 100644 index 0000000000..940a476916 --- /dev/null +++ b/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 WatchResponseAsync(Action<(uint response, IDictionary results)> handler, Action onError = null); + } +} diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs new file mode 100644 index 0000000000..d1905a4569 --- /dev/null +++ b/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("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 ShowFileDialogAsync(FileDialog dialog, Window parent) + { + var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}"; + ObjectPath objectPath; + var options = new Dictionary(); + 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("org.freedesktop.portal.Request", objectPath); + var tsc = new TaskCompletionSource(); + 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 ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) + { + var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}"; + var options = new Dictionary + { + { "directory", true } + }; + var objectPath = await _fileChooser.OpenFileAsync(parentWindow, dialog.Title ?? string.Empty, options); + var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + var tsc = new TaskCompletionSource(); + 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; + } + } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 083b16c107..22ff8e8f97 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/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); } diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index a93fb6831d..3fb0d43342 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -40,53 +40,49 @@ - + + + + + + + + - - - - - - - - - + + + - + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - + - - - - - - - + + + + + + + - + - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ec3f29c806..fa7ae69759 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/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().ToConstant(new X11Clipboard(this)) .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) - .Bind().ToConstant(new GtkSystemDialog()) + .Bind().ToConstant(DBusSystemDialog.TryCreate() as ISystemDialogImpl ?? new ManagedFileDialogExtensions.ManagedSystemDialogImpl()) .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()) .Bind().ToConstant(new X11PlatformLifetimeEvents(this)); @@ -209,10 +209,10 @@ namespace Avalonia public bool OverlayPopups { get; set; } /// - /// 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. /// - public bool UseDBusMenu { get; set; } + public bool UseDBusMenu { get; set; } = true; /// /// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true. diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 908b0ffa47..777e907617 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/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()) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor index 584c77a62c..dd7eb0ec54 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor @@ -1,19 +1,52 @@ 
+ onkeyup="@OnKeyUp" + onpointerdown="@OnPointerDown" + onpointerup="@OnPointerUp" + onpointermove="@OnPointerMove"> - - - + + +
+ +
+ + diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 1ccf53943a..7531dbf681 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/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().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(); } } } diff --git a/src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs b/src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs new file mode 100644 index 0000000000..bafc07ca15 --- /dev/null +++ b/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 GetTextAsync() + { + return await AvaloniaLocator.Current.GetRequiredService(). + InvokeAsync("navigator.clipboard.readText"); + } + + public async Task SetTextAsync(string text) + { + await AvaloniaLocator.Current.GetRequiredService(). + InvokeAsync("navigator.clipboard.writeText",text); + } + + public async Task ClearAsync() => await SetTextAsync(""); + + public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; + + public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); + + public Task GetDataAsync(string format) => Task.FromResult(new()); + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs b/src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs new file mode 100644 index 0000000000..48362b03c4 --- /dev/null +++ b/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 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(CreateDefaultChildSymbol); + return new JSObjectControlHandle(element); + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create) + { + Attachment? a = null; + try + { + using var hostElementJsReference = Invoke(GetReferenceSymbol, hostElement); + var child = create(new JSObjectControlHandle(hostElementJsReference)); + var attachmenetReference = Invoke(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(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)); + } + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts new file mode 100644 index 0000000000..baa9191845 --- /dev/null +++ b/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; + } +} diff --git a/src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs b/src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs new file mode 100644 index 0000000000..4426c3fbd7 --- /dev/null +++ b/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(); + } + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index 209a635a7b..a8a1a970dc 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/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(); } } diff --git a/src/Web/Avalonia.Web.Blazor/WinStubs.cs b/src/Web/Avalonia.Web.Blazor/WinStubs.cs index 7c30a96d35..a1fecef10e 100644 --- a/src/Web/Avalonia.Web.Blazor/WinStubs.cs +++ b/src/Web/Avalonia.Web.Blazor/WinStubs.cs @@ -8,21 +8,6 @@ using Avalonia.Platform; namespace Avalonia.Web.Blazor { - internal class ClipboardStub : IClipboard - { - public Task GetTextAsync() => Task.FromResult(""); - - public Task SetTextAsync(string text) => Task.CompletedTask; - - public Task ClearAsync() => Task.CompletedTask; - - public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; - - public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); - - public Task GetDataAsync(string format) => Task.FromResult(new ()); - } - internal class IconLoaderStub : IPlatformIconLoader { private class IconStub : IWindowIconImpl diff --git a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs b/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs index ac970d067f..0575533152 100644 --- a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs +++ b/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().ToSingleton() + .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(s_keyboard) .Bind().ToConstant(instance) diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index f4e4b00147..6e32d32913 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/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()) diff --git a/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs index 2a1628ea7d..fd05e780bf 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs +++ b/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); diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index e8108dd3de..0a47b152ed 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/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")] diff --git a/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs b/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs new file mode 100644 index 0000000000..f752936dc8 --- /dev/null +++ b/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 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(); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index ba01f3db40..b63cbd286e 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/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(menu.Parent); + Assert.NotNull(popup.Parent); + + window.Content = null; + Assert.Null(popup.Parent); + } + } + [Fact] public void Context_Menu_In_Resources_Can_Be_Shared() { diff --git a/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs index c2dd8cf01a..776b4508c2 100644 --- a/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs +++ b/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(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(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; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs index 3cebe142b6..4ffd314857 100644 --- a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs +++ b/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) diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Method.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Method.cs new file mode 100644 index 0000000000..e613a178d5 --- /dev/null +++ b/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; + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index bdd5cbbe2b..682fc622b8 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/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'> - + diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index a47638d2ec..a974e06385 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/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("aaaaaaaaaa".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + case 10: + return new TextCharacters(new ReadOnlySlice("bbbbbbbbbb".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + case 20: + return new TextCharacters(new ReadOnlySlice("cccccccccc".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + case 30: + return new TextCharacters(new ReadOnlySlice("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 - { - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text), ltrOptions), defaultProperties), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length, text.Length), ltrOptions), defaultProperties), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2, text.Length), rtlOptions), defaultProperties), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(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)); } } diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index 5f8854b3ab..4bc30484e9 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/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()) diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index c4b1e6c154..7c34bd192e 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/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);