diff --git a/src/Uno.UI.Runtime.Skia.MacOS/MacOSMetalRenderer.cs b/src/Uno.UI.Runtime.Skia.MacOS/MacOSMetalRenderer.cs index 88ad6fd08cc8..32278f2a529e 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/MacOSMetalRenderer.cs +++ b/src/Uno.UI.Runtime.Skia.MacOS/MacOSMetalRenderer.cs @@ -15,7 +15,7 @@ internal static class MacOSMetalRenderer // FIXME: contribute some extra API (e.g. using `nint` or `IntPtr`) to SkiaSharp to avoid reflection // net8+ alternative -> https://steven-giesel.com/blogPost/05ecdd16-8dc4-490f-b1cf-780c994346a4 var get = typeof(GRContext).GetMethod("GetObject", BindingFlags.Static | BindingFlags.NonPublic); - var context = (GRContext?)get?.Invoke(null, new object[] { ctx, true }); + var context = (GRContext?)get?.Invoke(null, [ctx, true]); if (context is null) { // Macs since 2012 have Metal 2 support and macOS 10.14 Mojave (2018) requires Metal diff --git a/src/Uno.UI.Runtime.Skia.MacOS/MacOSNativeElementHostingExtension.cs b/src/Uno.UI.Runtime.Skia.MacOS/MacOSNativeElementHostingExtension.cs new file mode 100644 index 000000000000..08c2038fab74 --- /dev/null +++ b/src/Uno.UI.Runtime.Skia.MacOS/MacOSNativeElementHostingExtension.cs @@ -0,0 +1,157 @@ +#nullable enable + +using Windows.Foundation; +using Windows.UI.Core; +using Microsoft.UI.Xaml.Controls; + +using Uno.Foundation.Extensibility; +using Uno.Foundation.Logging; + +namespace Uno.UI.Runtime.Skia.MacOS; + +internal class MacOSNativeElement : Microsoft.UI.Xaml.FrameworkElement +{ + public nint NativeHandle { get; internal set; } + + internal bool Detached { get; set; } +} + +internal class MacOSNativeElementHostingExtension : ContentPresenter.INativeElementHostingExtension +{ + private readonly ContentPresenter _presenter; + private readonly MacOSWindowNative? _window; + + private MacOSNativeElementHostingExtension(ContentPresenter contentPresenter) + { + _presenter = contentPresenter; + _window = _presenter.XamlRoot?.HostWindow?.NativeWindow as MacOSWindowNative; + } + + public static void Register() => ApiExtensibility.Register(typeof(ContentPresenter.INativeElementHostingExtension), o => new MacOSNativeElementHostingExtension(o)); + + public void ArrangeNativeElement(object content, Rect arrangeRect, Rect clipRect) + { + if (content is MacOSNativeElement element) + { + if (element.Detached) + { + this.Log().Debug($"Cannot arrange element `{nameof(content)}` of type {content.GetType().FullName} since it was detached."); + } + else + { + NativeUno.uno_native_arrange(element.NativeHandle, arrangeRect.Left, arrangeRect.Top, arrangeRect.Width, arrangeRect.Height, clipRect.Left, clipRect.Top, clipRect.Width, clipRect.Height); + } + } + else if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().Debug($"Object `{nameof(content)}` is a {content.GetType().FullName} and not a MacOSNativeElement subclass."); + } + } + + public void AttachNativeElement(object content) + { + if (content is MacOSNativeElement element) + { + NativeUno.uno_native_attach(element.NativeHandle); + } + else if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().Debug($"Object `{nameof(content)}` is a {content.GetType().FullName} and not a MacOSNativeElement subclass."); + } + } + + public void ChangeNativeElementOpacity(object content, double opacity) + { + if (content is MacOSNativeElement element) + { + // https://developer.apple.com/documentation/appkit/nsview/1483560-alphavalue?language=objc + // note: no marshaling needed as CGFloat is double for 64bits apps + NativeUno.uno_native_set_opacity(element.NativeHandle, opacity); + } + else if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().Debug($"Object `{nameof(content)}` is a {content.GetType().FullName} and not a MacOSNativeElement subclass."); + } + } + + public void ChangeNativeElementVisibility(object content, bool visible) + { + if (content is MacOSNativeElement element) + { + // https://developer.apple.com/documentation/appkit/nsview/1483369-hidden?language=objc + NativeUno.uno_native_set_visibility(element.NativeHandle, visible); + } + else if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().Debug($"Object `{nameof(content)}` is a {content.GetType().FullName} and not a MacOSNativeElement subclass."); + } + } + + public object? CreateSampleComponent(string text) + { + if (_window is null) + { + if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().Debug($"CreateSampleComponent failed as no MacOSWindowNative instance could be found."); + } + return null; + } + + var handle = NativeUno.uno_native_create_sample(_window.Handle, text); + return new MacOSNativeElement() + { + NativeHandle = handle, + AccessKey = text // FIXME: debug helper, to be removed + }; + } + + public void DetachNativeElement(object content) + { + if (content is MacOSNativeElement element) + { + if (element.Detached) + { + this.Log().Debug($"Object `{nameof(content)}` of type {content.GetType().FullName} was already detached."); + } + else + { + NativeUno.uno_native_detach(element.NativeHandle); + element.Detached = true; + } + } + else if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().Debug($"Object `{nameof(content)}` is a {content.GetType().FullName} and not a MacOSNativeElement subclass."); + } + } + + public bool IsNativeElement(object content) => content is MacOSNativeElement; + + public bool IsNativeElementAttached(object owner, object nativeElement) + { + if (nativeElement is MacOSNativeElement element) + { + return NativeUno.uno_native_is_attached(element.NativeHandle); + } + else if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().Debug($"Object `{nameof(owner)}` is a {owner.GetType().FullName} and not a MacOSNativeElement subclass."); + } + return false; + } + + public Size MeasureNativeElement(object content, Size childMeasuredSize, Size availableSize) + { + if (content is MacOSNativeElement element) + { + NativeUno.uno_native_measure(element.NativeHandle, childMeasuredSize.Width, childMeasuredSize.Height, availableSize.Width, availableSize.Height, out var width, out var height); + return new Size(width, height); + } + else if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().Debug($"Object `{nameof(content)}` is a {content.GetType().FullName} and not a MacOSNativeElement subclass."); + } + return Size.Empty; + } +} diff --git a/src/Uno.UI.Runtime.Skia.MacOS/MacOSWindowHost.cs b/src/Uno.UI.Runtime.Skia.MacOS/MacOSWindowHost.cs index 30560bb6d901..ee89a9fcf44d 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/MacOSWindowHost.cs +++ b/src/Uno.UI.Runtime.Skia.MacOS/MacOSWindowHost.cs @@ -12,11 +12,13 @@ using Windows.UI.Core; using Windows.UI.Input; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Input; using Window = Microsoft.UI.Xaml.Window; using Uno.Foundation.Extensibility; using Uno.Foundation.Logging; +using Uno.UI.Helpers; using Uno.UI.Hosting; namespace Uno.UI.Runtime.Skia.MacOS; @@ -71,7 +73,7 @@ private void UpdateWindowSize(double nativeWidth, double nativeHeight) SizeChanged?.Invoke(this, new Size(nativeWidth, nativeHeight)); } - private void Draw(SKSurface surface) + private void Draw(double nativeWidth, double nativeHeight, SKSurface surface) { using var canvas = surface.Canvas; using (new SKAutoCanvasRestore(canvas, true)) @@ -80,7 +82,16 @@ private void Draw(SKSurface surface) if (RootElement?.Visual is { } rootVisual) { - RootElement.XamlRoot?.Compositor.RenderRootVisual(surface, rootVisual, null); + int width = (int)nativeWidth; + int height = (int)nativeHeight; + var path = SkiaRenderHelper.RenderRootVisualAndReturnPath(width, height, rootVisual, surface); + if (path is { }) + { + using var negativePath = new SKPath(); + negativePath.AddRect(new SKRect(0, 0, width, height)); + using var diffPath = negativePath.Op(path, SKPathOp.Difference); + NativeUno.uno_window_clip_svg(_nativeWindow.Handle, diffPath.ToSvgPathData()); + } } } @@ -115,7 +126,7 @@ private void MetalDraw(double nativeWidth, double nativeHeight, nint texture) surface.Canvas.Scale(scale, scale); - Draw(surface); + Draw(nativeWidth, nativeHeight, surface); _context?.Flush(); } @@ -155,7 +166,7 @@ private unsafe void SoftDraw(double nativeWidth, double nativeHeight, nint* data _rowBytes = info.RowBytes; } - Draw(_surface!); + Draw(nativeWidth, nativeHeight, _surface!); *data = _bitmap.GetPixels(out var bitmapSize); *size = (int)bitmapSize; @@ -287,8 +298,7 @@ private static int OnRawKeyDown(nint handle, VirtualKey key, VirtualKeyModifiers } var args = CreateArgs(key, mods, scanCode, unicode); keyDown.Invoke(window!, args); - // we tell macOS it's always handled as WinUI does not mark as handled some keys that would make it beep in common cases - return 1; + return FocusManager.GetFocusedElement() == null ? 0 : 1; } catch (Exception e) { @@ -315,7 +325,7 @@ private static int OnRawKeyUp(nint handle, VirtualKey key, VirtualKeyModifiers m } var args = CreateArgs(key, mods, scanCode, unicode); keyUp.Invoke(window!, args); - return args.Handled ? 1 : 0; + return 1; } catch (Exception e) { @@ -423,8 +433,8 @@ internal static unsafe int OnMouseEvent(nint handle, NativeMouseEventData* data) } mouseEvent(window, BuildPointerArgs(*data)); - // let the window be activated (becoming the keyWindow) when clicked - return data->EventType == NativeMouseEvents.Down ? 0 : 1; + // always let the native side know about the mouse events, e.g. setting keyWindow, embedded native controls + return 0; } catch (Exception e) { diff --git a/src/Uno.UI.Runtime.Skia.MacOS/MacSkiaHost.cs b/src/Uno.UI.Runtime.Skia.MacOS/MacSkiaHost.cs index c1a90852a4c4..04e9a8d9f29a 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/MacSkiaHost.cs +++ b/src/Uno.UI.Runtime.Skia.MacOS/MacSkiaHost.cs @@ -25,6 +25,7 @@ static MacSkiaHost() MacOSFileSavePickerExtension.Register(); MacOSFolderPickerExtension.Register(); MacOSLauncherExtension.Register(); + MacOSNativeElementHostingExtension.Register(); MacOSNativeWindowFactoryExtension.Register(); MacOSSystemNavigationManagerPreviewExtension.Register(); MacOSSystemThemeHelperExtension.Register(); diff --git a/src/Uno.UI.Runtime.Skia.MacOS/NativeUno.cs b/src/Uno.UI.Runtime.Skia.MacOS/NativeUno.cs index 7728808828b4..30bb1dc9561a 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/NativeUno.cs +++ b/src/Uno.UI.Runtime.Skia.MacOS/NativeUno.cs @@ -219,7 +219,7 @@ internal static unsafe partial void uno_set_window_events_callbacks( internal static partial string uno_window_get_title(nint window); [LibraryImport("libUnoNativeMac.dylib", StringMarshalling = StringMarshalling.Utf8)] - internal static partial nint uno_window_set_title(nint window, string title); + internal static partial void uno_window_set_title(nint window, string title); [LibraryImport("libUnoNativeMac.dylib")] internal static unsafe partial void uno_set_window_close_callbacks( @@ -277,6 +277,9 @@ internal static unsafe partial void uno_set_window_close_callbacks( [LibraryImport("libUnoNativeMac.dylib")] internal static partial void uno_window_set_min_size(nint window, double width, double height); + [LibraryImport("libUnoNativeMac.dylib", StringMarshalling = StringMarshalling.Utf8)] + internal static partial void uno_window_clip_svg(nint window, string? svg); + [LibraryImport("libUnoNativeMac.dylib", StringMarshalling = StringMarshalling.Utf8)] internal static partial string? /* const char* _Nullable */ uno_pick_single_folder(string? prompt, string? identifier, int suggestedStartLocation); @@ -320,4 +323,29 @@ internal static unsafe partial void uno_set_window_close_callbacks( [LibraryImport("libUnoNativeMac.dylib")] [return: MarshalAs(UnmanagedType.I1)] internal static partial bool uno_cursor_set(CoreCursorType cursorType); + + [LibraryImport("libUnoNativeMac.dylib", StringMarshalling = StringMarshalling.Utf8)] + internal static partial nint uno_native_create_sample(nint window, string text); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_native_arrange(nint element, double arrangeLeft, double arrangeTop, double arrangeWidth, double arrangeHeight, double clipLeft, double clipTop, double clipWidth, double clipHeight); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_native_attach(nint element); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_native_detach(nint element); + + [LibraryImport("libUnoNativeMac.dylib")] + [return: MarshalAs(UnmanagedType.I1)] + internal static partial bool uno_native_is_attached(nint element); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_native_set_opacity(nint element, double opacity); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_native_set_visibility(nint element, [MarshalAs(UnmanagedType.I1)] bool visible); + + [LibraryImport("libUnoNativeMac.dylib")] + internal static partial void uno_native_measure(nint element, double childWidth, double childHeight, double availableWidth, double availableHeight, out double width, out double height); } diff --git a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac.xcodeproj/project.pbxproj b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac.xcodeproj/project.pbxproj index 7dbcf6cb66ec..0be42284d811 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac.xcodeproj/project.pbxproj +++ b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ D116C63E2AC79876004B975F /* UNOCursor.m in Sources */ = {isa = PBXBuildFile; fileRef = D116C63D2AC79876004B975F /* UNOCursor.m */; }; D13AB8A82B5839B200693F8E /* libSkiaSharp.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = D13AB8A72B5839B200693F8E /* libSkiaSharp.dylib */; }; + D18D4FAC2C2DE804003E4BBF /* UNONative.m in Sources */ = {isa = PBXBuildFile; fileRef = D18D4FAB2C2DE804003E4BBF /* UNONative.m */; }; D1A0651E2A7066F700101BE6 /* UNOMetalViewDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D1A0651D2A7066F700101BE6 /* UNOMetalViewDelegate.m */; }; D1A065202A8467B200101BE6 /* UNOApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = D1A0651F2A8467B200101BE6 /* UNOApplication.h */; }; D1A065222A84688000101BE6 /* UNOApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = D1A065212A84688000101BE6 /* UNOApplication.m */; }; @@ -24,6 +25,8 @@ D116C63D2AC79876004B975F /* UNOCursor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UNOCursor.m; sourceTree = ""; }; D13AB8A72B5839B200693F8E /* libSkiaSharp.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libSkiaSharp.dylib; path = UnoNativeMac/libSkiaSharp.dylib; sourceTree = ""; }; D13AB8AC2B58566400693F8E /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; + D18D4FAA2C2DE76F003E4BBF /* UNONative.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UNONative.h; sourceTree = ""; }; + D18D4FAB2C2DE804003E4BBF /* UNONative.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UNONative.m; sourceTree = ""; }; D1A0651C2A70664A00101BE6 /* UNOMetalViewDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UNOMetalViewDelegate.h; sourceTree = ""; }; D1A0651D2A7066F700101BE6 /* UNOMetalViewDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UNOMetalViewDelegate.m; sourceTree = ""; }; D1A0651F2A8467B200101BE6 /* UNOApplication.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UNOApplication.h; sourceTree = ""; }; @@ -90,6 +93,8 @@ D116C63D2AC79876004B975F /* UNOCursor.m */, D1A0651C2A70664A00101BE6 /* UNOMetalViewDelegate.h */, D1A0651D2A7066F700101BE6 /* UNOMetalViewDelegate.m */, + D18D4FAA2C2DE76F003E4BBF /* UNONative.h */, + D18D4FAB2C2DE804003E4BBF /* UNONative.m */, D1CC768E2ABA1368002A44F0 /* UNOPickers.h */, D1CC768C2ABA1337002A44F0 /* UNOPickers.m */, D1FE7A2C2B75C8E100ACFC76 /* UNOSoftView.h */, @@ -176,6 +181,7 @@ D1A065252A8AC23800101BE6 /* UNOWindow.m in Sources */, D1CC768B2AB9D464002A44F0 /* UNOClipboard.m in Sources */, D116C63E2AC79876004B975F /* UNOCursor.m in Sources */, + D18D4FAC2C2DE804003E4BBF /* UNONative.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNONative.h b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNONative.h new file mode 100644 index 000000000000..e9cdec54ebe7 --- /dev/null +++ b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNONative.h @@ -0,0 +1,40 @@ +// +// UNONative.h +// + +#pragma once + +#import "UnoNativeMac.h" +#import "UNOWindow.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol UNONativeElement + +@property (nonatomic) bool visible; + +-(void) detach; + +@end + +@interface UNORedView : NSView + +@end + +NSView* uno_native_create_sample(NSWindow *window, const char* _Nullable text); + +void uno_native_arrange(NSView *element, double arrangeLeft, double arrangeTop, double arrangeWidth, double arrangeHeight, double clipLeft, double clipTop, double clipWidth, double clipHeight); + +void uno_native_attach(NSView* element); + +void uno_native_detach(NSView* element); + +bool uno_native_is_attached(NSView* element); + +void uno_native_measure(NSView* element, double childWidth, double childHeight, double availableWidth, double availableHeight, double* width, double* height); + +void uno_native_set_opacity(NSView* element, double opacity); + +void uno_native_set_visibility(NSView* element, bool visible); + +NS_ASSUME_NONNULL_END diff --git a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNONative.m b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNONative.m new file mode 100644 index 000000000000..27607d4ac8e5 --- /dev/null +++ b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNONative.m @@ -0,0 +1,141 @@ +// +// UNONative.m +// + +#import "UNONative.h" + +static NSMutableSet *elements; + +@implementation UNORedView : NSView + +// make the background red for easier tracking +- (BOOL)wantsUpdateLayer +{ + return !self.hidden; +} + +- (void)updateLayer +{ + self.layer.backgroundColor = NSColor.redColor.CGColor; +} + +@synthesize visible; + +- (void)detach { + // nothing needed +} + +@end + +NSView* uno_native_create_sample(NSWindow *window, const char* _Nullable text) +{ + // no NSLabel on macOS + NSTextField* label = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)]; + label.bezeled = NO; + label.drawsBackground = NO; + label.editable = NO; + label.selectable = NO; + label.stringValue = [NSString stringWithUTF8String:text]; + label.frame = NSMakeRect(0, 0, label.fittingSize.width, label.fittingSize.height); + + NSView* sample = [[UNORedView alloc] initWithFrame:label.frame]; + [sample addSubview:label]; +#if DEBUG + NSLog(@"uno_native_create_sample #%p label: %@", sample, label.stringValue); +#endif + [window.contentViewController.view addSubview:sample]; + return sample; +} + +void uno_native_arrange(NSView *element, double arrangeLeft, double arrangeTop, double arrangeWidth, double arrangeHeight, double clipLeft, double clipTop, double clipWidth, double clipHeight) +{ + if (!element || !element.visible) { +#if DEBUG + NSLog(@"uno_native_arrange %p is not visible - nothing to arrange", element); +#endif + return; + } + + NSRect clip = NSMakeRect(arrangeLeft + clipLeft, arrangeTop - clipTop, clipWidth, clipHeight); + element.hidden = NSIsEmptyRect(clip) || clipHeight <= 0 || clipWidth <= 0; + if (element.hidden) { +#if DEBUG + NSLog(@"uno_native_arrange %p hidden by clipping", element); +#endif + return; + } + + NSRect arrange = NSMakeRect(arrangeLeft + clipLeft, arrangeTop + clipTop, MIN(arrangeWidth, clipWidth), MIN(arrangeHeight, clipHeight)); + element.frame = arrange; +#if DEBUG + NSLog(@"uno_native_arrange %p arrange(%g,%g,%g,%g) clip(%g,%g,%g,%g) %s", element, + arrangeLeft, arrangeTop, arrangeWidth, arrangeHeight, + clipLeft, clipTop, clipWidth, clipHeight, + element.hidden ? "EMPTY" : (clipWidth < arrangeWidth) || (clipHeight < arrangeHeight) ? "partial" : ""); +#endif +} + +void uno_native_attach(NSView* element) +{ +#if DEBUG + NSLog(@"uno_native_attach %p -> %s attached", element, [elements containsObject:element] ? "already" : "not previously"); +#endif + if (!elements) { + elements = [[NSMutableSet alloc] initWithCapacity:10]; + } + // note: it's too early to add a mask since the layer has not been set yet + [elements addObject:element]; +} + +void uno_native_detach(NSView *element) +{ +#if DEBUG + NSLog(@"uno_native_detach %p", element); +#endif + element.layer.mask = nil; + if (elements) { + if ([element conformsToProtocol:@protocol(UNONativeElement)]) { + id native = (id) element; + [native detach]; + } + [elements removeObject:element]; + } +} + +bool uno_native_is_attached(NSView* element) +{ + bool attached = elements ? [elements containsObject:element] : NO; +#if DEBUG + NSLog(@"uno_native_is_attached %s", attached ? "YES" : "NO"); +#endif + return attached; +} + +void uno_native_measure(NSView* element, double childWidth, double childHeight, double availableWidth, double availableHeight, double* width, double* height) +{ + CGSize size = element.subviews.firstObject.frame.size; + *width = size.width; + *height = size.height; +#if DEBUG + NSLog(@"uno_native_measure %p : child %g x %g / available %g x %g -> %g x %g", element, childWidth, childHeight, availableWidth, availableHeight, *width, *height); +#endif +} + +void uno_native_set_opacity(NSView* element, double opacity) +{ +#if DEBUG + NSLog(@"uno_native_set_opacity #%p : %g -> %g", element, element.alphaValue, opacity); +#endif + element.alphaValue = opacity; +} + +void uno_native_set_visibility(NSView* element, bool visible) +{ +#if DEBUG + NSLog(@"uno_native_set_visibility #%p : hidden %s -> visible %s", element, element.hidden ? "TRUE" : "FALSE", visible ? "TRUE" : "FALSE"); +#endif + element.visible = visible; + // hidden is controlled by both visible and clipping + if (!visible) + element.hidden = true; +} diff --git a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.h b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.h index 743cebd1daf4..c74c3344bd75 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.h +++ b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.h @@ -62,6 +62,7 @@ void uno_window_exit_full_screen(NSWindow *window); void uno_window_minimize(NSWindow *window, bool activateWindow); void uno_window_restore(NSWindow *window, bool activateWindow); +void uno_window_clip_svg(UNOWindow* window, const char* svg); typedef NS_ENUM(sint32, OverlappedPresenterState) { OverlappedPresenterStateMaximized, @@ -331,4 +332,13 @@ void uno_set_window_screen_change_callbacks(window_did_change_screen_fn_ptr scre void uno_window_notify_screen_change(NSWindow *window); + +@interface UNOMetalFlippedView : MTKView + +@property(getter=isFlipped, readonly) BOOL flipped; + +@property CAShapeLayer *clipLayer; + +@end + NS_ASSUME_NONNULL_END diff --git a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.m b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.m index 3b1e6cb9ca7c..6087ed76042d 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.m +++ b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOWindow.m @@ -1,5 +1,5 @@ // -// UNOWindowDelegate.m +// UNOWindow.m // #import "UNOWindow.h" @@ -87,6 +87,23 @@ - (void) applicationDidChangeScreenParametersNotification:(NSNotification*) note return main_window; } +@implementation UNOMetalFlippedView : MTKView + +// behave like UIView/UWP/WinUI, where the origin is top/left, instead of bottom/left +-(BOOL) isFlipped { + return YES; +} + +-(instancetype) initWithFrame:(CGRect)frameRect device:(id)device { + self = [super initWithFrame:frameRect device:device]; + if (self) { + // TODO + } + return self; +} + +@end + NSWindow* uno_window_create(double width, double height) { CGRect size = NSMakeRect(0, 0, width, height); @@ -98,7 +115,7 @@ - (void) applicationDidChangeScreenParametersNotification:(NSNotification*) note id device = uno_application_get_metal_device(); if (device) { - MTKView *v = [[MTKView alloc] initWithFrame:size device:device]; + UNOMetalFlippedView *v = [[UNOMetalFlippedView alloc] initWithFrame:size device:device]; v.enableSetNeedsDisplay = YES; window.metalViewDelegate = [[UNOMetalViewDelegate alloc] initWithMetalKitView:v]; v.delegate = window.metalViewDelegate; @@ -639,6 +656,95 @@ void uno_set_window_close_callbacks(window_should_close_fn_ptr shouldClose, wind return gr_direct_context_make_metal(device, queue); } +CGFloat readNextCoord(const char *svg, int *position, long length) +{ + CGFloat result = NAN; + if (*position >= length) { +#if DEBUG + NSLog(@"uno_window_clip_svg readNextCoord position:%d >= length:%ld", *position, length); +#endif + return result; + } + const char* start = svg + *position; + char* end; + result = strtod(start, &end); + *position += (int)(end - start); + return result; +} + +void uno_window_clip_svg(UNOWindow* window, const char* svg) +{ + if (svg) { +#if DEBUG + NSLog(@"uno_window_clip_svg %@ %@ %s", window, window.contentView.layer.description, svg); +#endif + NSArray<__kindof NSView *> *subviews = window.contentViewController.view.subviews; + for (int i = 0; i < subviews.count; i++) { + NSView* view = subviews[i]; +#if DEBUG + NSLog(@"uno_window_clip_svg subview %d %@ layer %@ mask %@", i, view, view.layer, view.layer.mask); +#endif + CGMutablePathRef path = CGPathCreateMutable(); + // small subset of an SVG path parser handling trusted input of integer-based points + long length = strlen(svg); + for (int i=0; i < length;) { + CGFloat x, y; + char op = svg[i]; + switch (op) { + case 'M': + i++; // skip M + x = readNextCoord(svg, &i, length); + i++; // skip separator + y = readNextCoord(svg, &i, length); + // there might not be a separator (not required before the next op) +#if DEBUG_PARSER + NSLog(@"uno_window_clip_svg parsing CGPathMoveToPoint %g %g - position %d", x, y, i); +#endif + x -= view.frame.origin.x; + y -= view.frame.origin.y; + CGPathMoveToPoint(path, nil, x, y); + break; + case 'L': + i++; // skip L + x = readNextCoord(svg, &i, length); + i++; // skip separator + y = readNextCoord(svg, &i, length); + // there might not be a separator (not required before the next op) +#if DEBUG_PARSER + NSLog(@"uno_window_clip_svg parsing CGPathAddLineToPoint %g %g - position %d", x, y, i); +#endif + x -= view.frame.origin.x; + y -= view.frame.origin.y; + CGPathAddLineToPoint(path, nil, x, y); + break; + case 'Z': + i++; // skip Z +#if DEBUG_PARSER + NSLog(@"uno_window_clip_svg parsing CGPathCloseSubpath - position %d", i); +#endif + CGPathCloseSubpath(path); + break; +#if DEBUG + default: + if (op != ' ') { + NSLog(@"uno_window_clip_svg parsing unknown op %c at position %d", op, i); + } + i++; // skip unknown op + break; +#endif + } + } + CAShapeLayer* mask = view.layer.mask; + if (mask == nil) { + view.layer.mask = mask = [[CAShapeLayer alloc] init]; + } + mask.fillColor = NSColor.blueColor.CGColor; // anything but clearColor + mask.path = path; + mask.fillRule = kCAFillRuleEvenOdd; + } + } +} + @implementation UNOWindow : NSWindow + (void)initialize { @@ -738,7 +844,7 @@ - (void)sendEvent:(NSEvent *)event { UniChar unicode = get_unicode(event); handled = uno_get_window_key_up_callback()(self, get_virtual_key(scanCode), get_modifiers(event.modifierFlags), scanCode, unicode); #if DEBUG - NSLog(@"NSEventTypeKeyUp: %@ window %p unocode %d handled? %s", event, self, unicode, handled ? "true" : "false"); + NSLog(@"NSEventTypeKeyUp: %@ window %p unicode %d handled? %s", event, self, unicode, handled ? "true" : "false"); #endif break; } diff --git a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UnoNativeMac.h b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UnoNativeMac.h index d582469f2d3e..4fdaeaae44e2 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UnoNativeMac.h +++ b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UnoNativeMac.h @@ -5,3 +5,4 @@ #import #import #import +#import