diff --git a/src/Core/src/ScreenshotDispatch.cs b/src/Core/src/ScreenshotDispatch.cs index 66888d7690a4..33a11866300a 100644 --- a/src/Core/src/ScreenshotDispatch.cs +++ b/src/Core/src/ScreenshotDispatch.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Media; @@ -7,50 +6,36 @@ namespace Microsoft.Maui { /// /// Internal helper that routes - /// and through a keyed DI - /// hook when MAUI is built for a non-built-in platform TFM and therefore has no - /// compile-time screenshot implementation. + /// and through the registered + /// screenshot service when MAUI is built for a non-built-in platform TFM and + /// therefore has no compile-time screenshot implementation. /// /// - /// Third-party platform backends (e.g. macOS AppKit, Linux/GTK) register a - /// of to - /// of nullable - /// (i.e. Func<object, Task<IScreenshotResult?>>) under one of - /// the well-known keys defined on this type. The lambda receives the handler's - /// object and returns a task whose - /// result is the screenshot (or if capture is not - /// supported for that view). A hook that returns a - /// task is treated as unsupported and produces a result. - /// This contract intentionally uses only BCL types so it can ship without any - /// MAUI public API addition. + /// Third-party platform backends (e.g. macOS AppKit, Linux/GTK) register an + /// implementation that also implements + /// in the app's . + /// The dispatch resolves that service from the handler's + /// and forwards the handler's platform + /// view (or, for views, its container) to + /// . When no capable + /// service is registered (or capture is unsupported) the result is + /// , preserving the extension methods' graceful contract. /// static class ScreenshotDispatch { - /// - /// DI service key for the screenshot hook. - /// - public const string ViewCaptureKey = "Microsoft.Maui.ViewCapture"; - - /// - /// DI service key for the screenshot hook. - /// - public const string WindowCaptureKey = "Microsoft.Maui.WindowCapture"; - - public static Task CaptureAsync(IElementHandler? handler, string serviceKey) + public static Task CaptureAsync(IElementHandler? handler, object? captureView) { - var platformView = handler?.PlatformView; - if (platformView is null) + if (captureView is null) return Task.FromResult(null); - if (handler!.MauiContext?.Services is not IKeyedServiceProvider keyedProvider) - return Task.FromResult(null); - - var capture = keyedProvider.GetKeyedService>>(serviceKey); - - if (capture is null) + if (handler?.MauiContext?.Services?.GetService(typeof(IScreenshot)) is not IScreenshot screenshot + || !screenshot.IsCaptureSupported + || screenshot is not IViewScreenshot viewScreenshot) + { return Task.FromResult(null); + } - return capture(platformView) ?? Task.FromResult(null); + return viewScreenshot.CaptureViewAsync(captureView) ?? Task.FromResult(null); } } } diff --git a/src/Core/src/ViewExtensions.cs b/src/Core/src/ViewExtensions.cs index 73b30adebf30..d3b7b1c3088f 100644 --- a/src/Core/src/ViewExtensions.cs +++ b/src/Core/src/ViewExtensions.cs @@ -72,19 +72,19 @@ void BuildFlatList(IView view, List flatList) /// /// On non-built-in platform TFMs (e.g. net10.0-macos AppKit backends, /// net10.0 Linux/GTK backends) where MAUI does not ship a screenshot - /// implementation, capture is routed through a keyed DI hook. Third-party - /// platform backends can opt in by registering a - /// of to - /// Task<IScreenshotResult?> under the service key - /// "Microsoft.Maui.ViewCapture": + /// implementation, capture is routed through the registered screenshot service. + /// Third-party platform backends opt in by registering an + /// implementation that also implements in the app's + /// : /// - /// builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( - /// "Microsoft.Maui.ViewCapture", - /// (_, _) => platformView => ((AppKit.NSView)platformView).CaptureAsync()); + /// builder.Services.AddSingleton<IScreenshot, AppKitScreenshotImplementation>(); /// - /// If no hook is registered (or the - /// is ), the returned task resolves to - /// . + /// The dispatch resolves that service from the handler's + /// and forwards the view's container view + /// (or, failing that, its platform view) to + /// . When no capable service is + /// registered (or the is + /// ), the returned task resolves to . /// public static Task CaptureAsync(this IView view) { @@ -97,7 +97,13 @@ void BuildFlatList(IView view, List flatList) return CaptureAsync(platformView); #else - return ScreenshotDispatch.CaptureAsync(view?.Handler, ScreenshotDispatch.ViewCaptureKey); + // Prefer the container view (clip/shadow/border) like the #if PLATFORM path's + // view.ToPlatform() does; fall back to the raw platform view. The shared dispatch + // helper resolves the registered IViewScreenshot and stays graceful (returns null + // when capture is unavailable) to preserve this path's contract. + var handler = view?.Handler; + var captureView = (handler as IViewHandler)?.ContainerView ?? handler?.PlatformView; + return ScreenshotDispatch.CaptureAsync(handler, captureView); #endif } diff --git a/src/Core/src/WindowExtensions.cs b/src/Core/src/WindowExtensions.cs index 45cc0b6c7e0a..24be506a637e 100644 --- a/src/Core/src/WindowExtensions.cs +++ b/src/Core/src/WindowExtensions.cs @@ -22,19 +22,18 @@ public static partial class WindowExtensions /// /// On non-built-in platform TFMs (e.g. net10.0-macos AppKit backends, /// net10.0 Linux/GTK backends) where MAUI does not ship a screenshot - /// implementation, capture is routed through a keyed DI hook. Third-party - /// platform backends can opt in by registering a - /// of to - /// Task<IScreenshotResult?> under the service key - /// "Microsoft.Maui.WindowCapture": + /// implementation, capture is routed through the registered screenshot service. + /// Third-party platform backends opt in by registering an + /// implementation that also implements in the app's + /// : /// - /// builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( - /// "Microsoft.Maui.WindowCapture", - /// (_, _) => platformWindow => ((AppKit.NSWindow)platformWindow).CaptureAsync()); + /// builder.Services.AddSingleton<IScreenshot, AppKitScreenshotImplementation>(); /// - /// If no hook is registered (or the - /// is ), the returned task resolves to - /// . + /// The dispatch resolves that service from the handler's + /// and forwards the window's platform view + /// to . When no capable service + /// is registered (or the is + /// ), the returned task resolves to . /// public static Task CaptureAsync(this IWindow window) { @@ -47,7 +46,8 @@ public static partial class WindowExtensions return CaptureAsync(platformView); #else - return ScreenshotDispatch.CaptureAsync(window?.Handler, ScreenshotDispatch.WindowCaptureKey); + var handler = window?.Handler; + return ScreenshotDispatch.CaptureAsync(handler, handler?.PlatformView); #endif } diff --git a/src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs b/src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs deleted file mode 100644 index e96c8bba4788..000000000000 --- a/src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs +++ /dev/null @@ -1,233 +0,0 @@ -#nullable enable -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Maui.Media; -using NSubstitute; -using Xunit; - -namespace Microsoft.Maui.UnitTests.Extensions -{ - [System.ComponentModel.Category(TestCategory.Extensions)] - public class ScreenshotDispatchTests - { - // The test project targets $(_MauiDotNetTfm) = net10.0, so the #else - // branches of ViewExtensions.CaptureAsync / WindowExtensions.CaptureAsync - // are what gets compiled here. That's exactly the code path third-party - // platform backends hit, so these tests validate the real dispatch logic. - - sealed class FakeScreenshotResult : IScreenshotResult - { - public int Width => 0; - public int Height => 0; - public Task OpenReadAsync(ScreenshotFormat format = ScreenshotFormat.Png, int quality = 100) - => throw new NotImplementedException(); - public Task CopyToAsync(System.IO.Stream destination, ScreenshotFormat format = ScreenshotFormat.Png, int quality = 100) - => throw new NotImplementedException(); - } - - static (IView view, object platformView) CreateViewWithHandler(IServiceProvider services) - { - var platformView = new object(); - var handler = Substitute.For(); - handler.PlatformView.Returns(platformView); - - var context = Substitute.For(); - context.Services.Returns(services); - handler.MauiContext.Returns(context); - - var view = Substitute.For(); - view.Handler.Returns(handler); - - return (view, platformView); - } - - static (IWindow window, object platformView) CreateWindowWithHandler(IServiceProvider services) - { - var platformView = new object(); - var handler = Substitute.For(); - handler.PlatformView.Returns(platformView); - - var context = Substitute.For(); - context.Services.Returns(services); - handler.MauiContext.Returns(context); - - var window = Substitute.For(); - window.Handler.Returns(handler); - - return (window, platformView); - } - - [Fact] - public async Task ViewCaptureAsync_NullView_ReturnsNull() - { - IView? view = null; - var result = await view!.CaptureAsync(); - Assert.Null(result); - } - - [Fact] - public async Task ViewCaptureAsync_NoHandler_ReturnsNull() - { - var view = Substitute.For(); - view.Handler.Returns((IViewHandler?)null); - - var result = await view.CaptureAsync(); - Assert.Null(result); - } - - [Fact] - public async Task ViewCaptureAsync_NoPlatformView_ReturnsNull() - { - var handler = Substitute.For(); - handler.PlatformView.Returns((object?)null); - var view = Substitute.For(); - view.Handler.Returns(handler); - - var result = await view.CaptureAsync(); - Assert.Null(result); - } - - [Fact] - public async Task ViewCaptureAsync_NoKeyedHookRegistered_ReturnsNull() - { - var services = new ServiceCollection().BuildServiceProvider(); - var (view, _) = CreateViewWithHandler(services); - - var result = await view.CaptureAsync(); - Assert.Null(result); - } - - [Fact] - public async Task ViewCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView() - { - object? captured = null; - var expected = new FakeScreenshotResult(); - - var services = new ServiceCollection(); - services.AddKeyedSingleton>>( - "Microsoft.Maui.ViewCapture", - (_, _) => pv => - { - captured = pv; - return Task.FromResult(expected); - }); - - var (view, platformView) = CreateViewWithHandler(services.BuildServiceProvider()); - - var result = await view.CaptureAsync(); - - Assert.Same(platformView, captured); - Assert.Same(expected, result); - } - - [Fact] - public async Task ViewCaptureAsync_HookRegisteredUnderWrongKey_ReturnsNull() - { - var services = new ServiceCollection(); - services.AddKeyedSingleton>>( - "Microsoft.Maui.WindowCapture", // wrong key for a view - (_, _) => _ => Task.FromResult(new FakeScreenshotResult())); - - var (view, _) = CreateViewWithHandler(services.BuildServiceProvider()); - - var result = await view.CaptureAsync(); - Assert.Null(result); - } - - [Fact] - public async Task ViewCaptureAsync_HookReturnsNullResult_ReturnsNull() - { - var services = new ServiceCollection(); - services.AddKeyedSingleton>>( - "Microsoft.Maui.ViewCapture", - (_, _) => _ => Task.FromResult(null)); - - var (view, _) = CreateViewWithHandler(services.BuildServiceProvider()); - - var result = await view.CaptureAsync(); - Assert.Null(result); - } - - [Fact] - public async Task ViewCaptureAsync_HookReturnsNullTask_ReturnsNull() - { - var services = new ServiceCollection(); - services.AddKeyedSingleton>>( - "Microsoft.Maui.ViewCapture", - (_, _) => _ => null!); - - var (view, _) = CreateViewWithHandler(services.BuildServiceProvider()); - - var result = await view.CaptureAsync(); - Assert.Null(result); - } - - [Fact] - public async Task WindowCaptureAsync_NullWindow_ReturnsNull() - { - IWindow? window = null; - var result = await window!.CaptureAsync(); - Assert.Null(result); - } - - [Fact] - public async Task WindowCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView() - { - object? captured = null; - var expected = new FakeScreenshotResult(); - - var services = new ServiceCollection(); - services.AddKeyedSingleton>>( - "Microsoft.Maui.WindowCapture", - (_, _) => pv => - { - captured = pv; - return Task.FromResult(expected); - }); - - var (window, platformView) = CreateWindowWithHandler(services.BuildServiceProvider()); - - var result = await window.CaptureAsync(); - - Assert.Same(platformView, captured); - Assert.Same(expected, result); - } - - [Fact] - public async Task WindowCaptureAsync_HookRegisteredUnderWrongKey_ReturnsNull() - { - var services = new ServiceCollection(); - services.AddKeyedSingleton>>( - "Microsoft.Maui.ViewCapture", // wrong key for a window - (_, _) => _ => Task.FromResult(new FakeScreenshotResult())); - - var (window, _) = CreateWindowWithHandler(services.BuildServiceProvider()); - - var result = await window.CaptureAsync(); - Assert.Null(result); - } - - [Fact] - public async Task ViewAndWindowHooks_CoexistWithoutInterference() - { - var viewResult = new FakeScreenshotResult(); - var windowResult = new FakeScreenshotResult(); - - var services = new ServiceCollection(); - services.AddKeyedSingleton>>( - "Microsoft.Maui.ViewCapture", - (_, _) => _ => Task.FromResult(viewResult)); - services.AddKeyedSingleton>>( - "Microsoft.Maui.WindowCapture", - (_, _) => _ => Task.FromResult(windowResult)); - var provider = services.BuildServiceProvider(); - - var (view, _) = CreateViewWithHandler(provider); - var (window, _) = CreateWindowWithHandler(provider); - - Assert.Same(viewResult, await view.CaptureAsync()); - Assert.Same(windowResult, await window.CaptureAsync()); - } - } -} diff --git a/src/Core/tests/UnitTests/Extensions/ViewExtensionsCaptureTests.cs b/src/Core/tests/UnitTests/Extensions/ViewExtensionsCaptureTests.cs new file mode 100644 index 000000000000..57f0e6ea6dce --- /dev/null +++ b/src/Core/tests/UnitTests/Extensions/ViewExtensionsCaptureTests.cs @@ -0,0 +1,287 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Media; +using NSubstitute; +using Xunit; + +namespace Microsoft.Maui.UnitTests.Extensions +{ + [System.ComponentModel.Category(TestCategory.Extensions)] + public class ViewExtensionsCaptureTests + { + [Fact] + public async Task CaptureAsync_View_ReturnsNull_WhenHandlerIsNull() + { + var view = Substitute.For(); + view.Handler.Returns((IViewHandler)null); + + var result = await view.CaptureAsync(); + + Assert.Null(result); + } + + [Fact] + public async Task CaptureAsync_View_ReturnsNull_WhenPlatformViewIsNull() + { + var handler = Substitute.For(); + handler.PlatformView.Returns((object)null); + + var view = Substitute.For(); + view.Handler.Returns(handler); + + var result = await view.CaptureAsync(); + + Assert.Null(result); + } + + [Fact] + public async Task CaptureAsync_View_ReturnsNull_WhenScreenshotServiceNotRegistered() + { + var services = new ServiceCollection().BuildServiceProvider(); + var mauiContext = new MauiContext(services); + + var handler = Substitute.For(); + handler.PlatformView.Returns(new object()); + handler.MauiContext.Returns(mauiContext); + + var view = Substitute.For(); + view.Handler.Returns(handler); + + var result = await view.CaptureAsync(); + + Assert.Null(result); + } + + [Fact] + public async Task CaptureAsync_View_ReturnsNull_WhenCaptureNotSupported() + { + var screenshot = Substitute.For(); + screenshot.IsCaptureSupported.Returns(false); + + var services = new ServiceCollection() + .AddSingleton(screenshot) + .BuildServiceProvider(); + var mauiContext = new MauiContext(services); + + var handler = Substitute.For(); + handler.PlatformView.Returns(new object()); + handler.MauiContext.Returns(mauiContext); + + var view = Substitute.For(); + view.Handler.Returns(handler); + + var result = await view.CaptureAsync(); + + Assert.Null(result); + } + + [Fact] + public async Task CaptureAsync_View_ReturnsNull_WhenScreenshotDoesNotImplementIViewScreenshot() + { + var screenshot = Substitute.For(); + screenshot.IsCaptureSupported.Returns(true); + + var services = new ServiceCollection() + .AddSingleton(screenshot) + .BuildServiceProvider(); + var mauiContext = new MauiContext(services); + + var handler = Substitute.For(); + handler.PlatformView.Returns(new object()); + handler.MauiContext.Returns(mauiContext); + + var view = Substitute.For(); + view.Handler.Returns(handler); + + var result = await view.CaptureAsync(); + + Assert.Null(result); + } + + [Fact] + public async Task CaptureAsync_View_ReturnsCaptureResult_WhenViewScreenshotRegistered() + { + var expectedResult = Substitute.For(); + var platformView = new object(); + + var screenshot = Substitute.For(); + screenshot.IsCaptureSupported.Returns(true); + ((IViewScreenshot)screenshot).CaptureViewAsync(platformView) + .Returns(Task.FromResult(expectedResult)); + + var services = new ServiceCollection() + .AddSingleton(screenshot) + .BuildServiceProvider(); + var mauiContext = new MauiContext(services); + + var handler = Substitute.For(); + handler.PlatformView.Returns(platformView); + handler.MauiContext.Returns(mauiContext); + + var view = Substitute.For(); + view.Handler.Returns(handler); + + var result = await view.CaptureAsync(); + + Assert.Same(expectedResult, result); + await ((IViewScreenshot)screenshot).Received(1).CaptureViewAsync(platformView); + } + + [Fact] + public async Task CaptureAsync_Window_ReturnsNull_WhenHandlerIsNull() + { + var window = Substitute.For(); + window.Handler.Returns((IElementHandler)null); + + var result = await window.CaptureAsync(); + + Assert.Null(result); + } + + [Fact] + public async Task CaptureAsync_Window_ReturnsNull_WhenScreenshotServiceNotRegistered() + { + var services = new ServiceCollection().BuildServiceProvider(); + var mauiContext = new MauiContext(services); + + var handler = Substitute.For(); + handler.PlatformView.Returns(new object()); + handler.MauiContext.Returns(mauiContext); + + var window = Substitute.For(); + window.Handler.Returns(handler); + + var result = await window.CaptureAsync(); + + Assert.Null(result); + } + + [Fact] + public async Task CaptureAsync_Window_ReturnsNull_WhenScreenshotDoesNotImplementIViewScreenshot() + { + var screenshot = Substitute.For(); + screenshot.IsCaptureSupported.Returns(true); + + var services = new ServiceCollection() + .AddSingleton(screenshot) + .BuildServiceProvider(); + var mauiContext = new MauiContext(services); + + var handler = Substitute.For(); + handler.PlatformView.Returns(new object()); + handler.MauiContext.Returns(mauiContext); + + var window = Substitute.For(); + window.Handler.Returns(handler); + + var result = await window.CaptureAsync(); + + Assert.Null(result); + } + + [Fact] + public async Task CaptureAsync_Window_ReturnsCaptureResult_WhenViewScreenshotRegistered() + { + var expectedResult = Substitute.For(); + var platformView = new object(); + + var screenshot = Substitute.For(); + screenshot.IsCaptureSupported.Returns(true); + ((IViewScreenshot)screenshot).CaptureViewAsync(platformView) + .Returns(Task.FromResult(expectedResult)); + + var services = new ServiceCollection() + .AddSingleton(screenshot) + .BuildServiceProvider(); + var mauiContext = new MauiContext(services); + + var handler = Substitute.For(); + handler.PlatformView.Returns(platformView); + handler.MauiContext.Returns(mauiContext); + + var window = Substitute.For(); + window.Handler.Returns(handler); + + var result = await window.CaptureAsync(); + + Assert.Same(expectedResult, result); + await ((IViewScreenshot)screenshot).Received(1).CaptureViewAsync(platformView); + } + + [Fact] + public async Task CaptureAsync_View_CapturesContainerView_WhenContainerViewPresent() + { + // Parity with the #if PLATFORM path (view.ToPlatform()): when the handler has a + // container view (clip/shadow/border), it must be captured, not the inner PlatformView. + var expectedResult = Substitute.For(); + var platformView = new object(); + var containerView = new object(); + + var screenshot = Substitute.For(); + screenshot.IsCaptureSupported.Returns(true); + ((IViewScreenshot)screenshot).CaptureViewAsync(containerView) + .Returns(Task.FromResult(expectedResult)); + + var services = new ServiceCollection() + .AddSingleton(screenshot) + .BuildServiceProvider(); + var mauiContext = new MauiContext(services); + + var handler = Substitute.For(); + handler.PlatformView.Returns(platformView); + handler.ContainerView.Returns(containerView); + handler.MauiContext.Returns(mauiContext); + + var view = Substitute.For(); + view.Handler.Returns(handler); + + var result = await view.CaptureAsync(); + + Assert.Same(expectedResult, result); + await ((IViewScreenshot)screenshot).Received(1).CaptureViewAsync(containerView); + await ((IViewScreenshot)screenshot).DidNotReceive().CaptureViewAsync(platformView); + } + + [Fact] + public async Task CaptureAsync_View_ReturnsNull_WhenMauiContextIsNull() + { + var handler = Substitute.For(); + handler.PlatformView.Returns(new object()); + handler.MauiContext.Returns((IMauiContext)null); + + var view = Substitute.For(); + view.Handler.Returns(handler); + + var result = await view.CaptureAsync(); + + Assert.Null(result); + } + + [Fact] + public async Task CaptureAsync_View_ReturnsNull_WhenViewScreenshotReturnsNull() + { + var screenshot = Substitute.For(); + screenshot.IsCaptureSupported.Returns(true); + ((IViewScreenshot)screenshot).CaptureViewAsync(Arg.Any()) + .Returns(Task.FromResult(null)); + + var services = new ServiceCollection() + .AddSingleton(screenshot) + .BuildServiceProvider(); + var mauiContext = new MauiContext(services); + + var handler = Substitute.For(); + handler.PlatformView.Returns(new object()); + handler.MauiContext.Returns(mauiContext); + + var view = Substitute.For(); + view.Handler.Returns(handler); + + var result = await view.CaptureAsync(); + + Assert.Null(result); + } + } +} diff --git a/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index b6e937715e5e..bb6b24e12622 100644 --- a/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -26,3 +26,5 @@ override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(Syste override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object? static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task?>! +Microsoft.Maui.Media.IViewScreenshot +Microsoft.Maui.Media.IViewScreenshot.CaptureViewAsync(object! platformView) -> System.Threading.Tasks.Task! diff --git a/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 913892750f18..bf902cf6b1a7 100644 --- a/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -13,3 +13,5 @@ override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.C static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task?>! ~override Microsoft.Maui.ApplicationModel.Permissions.PostNotifications.CheckStatusAsync() -> System.Threading.Tasks.Task ~override Microsoft.Maui.ApplicationModel.Permissions.PostNotifications.RequestAsync() -> System.Threading.Tasks.Task +Microsoft.Maui.Media.IViewScreenshot +Microsoft.Maui.Media.IViewScreenshot.CaptureViewAsync(object! platformView) -> System.Threading.Tasks.Task! diff --git a/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 913892750f18..bf902cf6b1a7 100644 --- a/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -13,3 +13,5 @@ override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.C static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task?>! ~override Microsoft.Maui.ApplicationModel.Permissions.PostNotifications.CheckStatusAsync() -> System.Threading.Tasks.Task ~override Microsoft.Maui.ApplicationModel.Permissions.PostNotifications.RequestAsync() -> System.Threading.Tasks.Task +Microsoft.Maui.Media.IViewScreenshot +Microsoft.Maui.Media.IViewScreenshot.CaptureViewAsync(object! platformView) -> System.Threading.Tasks.Task! diff --git a/src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index f80ea6319128..c9b13435c313 100644 --- a/src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -11,3 +11,5 @@ static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storag ~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type sourceType) -> bool ~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) -> object? ~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) -> object? +Microsoft.Maui.Media.IViewScreenshot +Microsoft.Maui.Media.IViewScreenshot.CaptureViewAsync(object! platformView) -> System.Threading.Tasks.Task! diff --git a/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index f4d3f71144bf..0963fc6a00d1 100644 --- a/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -11,3 +11,5 @@ override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(Syste override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object? static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task?>! +Microsoft.Maui.Media.IViewScreenshot +Microsoft.Maui.Media.IViewScreenshot.CaptureViewAsync(object! platformView) -> System.Threading.Tasks.Task! diff --git a/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt index f4d3f71144bf..0963fc6a00d1 100644 --- a/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt @@ -11,3 +11,5 @@ override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(Syste override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object? static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task?>! +Microsoft.Maui.Media.IViewScreenshot +Microsoft.Maui.Media.IViewScreenshot.CaptureViewAsync(object! platformView) -> System.Threading.Tasks.Task! diff --git a/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt index f4d3f71144bf..0963fc6a00d1 100644 --- a/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -11,3 +11,5 @@ override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(Syste override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object? override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object? static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task?>! +Microsoft.Maui.Media.IViewScreenshot +Microsoft.Maui.Media.IViewScreenshot.CaptureViewAsync(object! platformView) -> System.Threading.Tasks.Task! diff --git a/src/Essentials/src/Screenshot/Screenshot.android.cs b/src/Essentials/src/Screenshot/Screenshot.android.cs index 0b8f1a32fc38..5ca9312ccbf8 100644 --- a/src/Essentials/src/Screenshot/Screenshot.android.cs +++ b/src/Essentials/src/Screenshot/Screenshot.android.cs @@ -11,7 +11,7 @@ namespace Microsoft.Maui.Media { - partial class ScreenshotImplementation : IPlatformScreenshot, IScreenshot + partial class ScreenshotImplementation : IPlatformScreenshot, IScreenshot, IViewScreenshot { static IWindowManager WindowManager => Application.Context.GetSystemService(Context.WindowService) as IWindowManager; @@ -47,6 +47,11 @@ public Task CaptureAsync(View view) return Task.FromResult(result); } +#nullable enable annotations + public Task CaptureViewAsync(object platformView) => + platformView is View view ? CaptureAsync(view)! : Task.FromResult(null); +#nullable restore + static Bitmap Render(View view) { var bitmap = RenderUsingCanvasDrawing(view); diff --git a/src/Essentials/src/Screenshot/Screenshot.ios.cs b/src/Essentials/src/Screenshot/Screenshot.ios.cs index 0c6c263453bb..79abd4e4c930 100644 --- a/src/Essentials/src/Screenshot/Screenshot.ios.cs +++ b/src/Essentials/src/Screenshot/Screenshot.ios.cs @@ -13,7 +13,7 @@ namespace Microsoft.Maui.Media { - partial class ScreenshotImplementation : IPlatformScreenshot, IScreenshot + partial class ScreenshotImplementation : IPlatformScreenshot, IScreenshot, IViewScreenshot { public bool IsCaptureSupported => true; @@ -114,6 +114,9 @@ public Task CaptureAsync(UIWindow window) return Task.FromResult(result); } + public Task CaptureViewAsync(object platformView) => + platformView is UIView view ? CaptureAsync(view) : Task.FromResult(null); + static bool TryRender(UIView view, out Exception? error) { try diff --git a/src/Essentials/src/Screenshot/Screenshot.shared.cs b/src/Essentials/src/Screenshot/Screenshot.shared.cs index 129cf8296f4d..7e2204514d6f 100644 --- a/src/Essentials/src/Screenshot/Screenshot.shared.cs +++ b/src/Essentials/src/Screenshot/Screenshot.shared.cs @@ -100,6 +100,22 @@ public interface IPlatformScreenshot : IScreenshot #endif } + /// + /// Provides platform-agnostic view-level screenshot capture. + /// Third-party platform backends should implement this interface on their implementation + /// to enable element-level screenshot capture. + /// + public interface IViewScreenshot + { + /// + /// Captures a screenshot of a platform view. + /// The implementation should cast to the appropriate platform type. + /// + /// The platform-specific view object to capture. + /// An with the captured screenshot, or if capture is not supported for the given view type. + Task CaptureViewAsync(object platformView); + } + /// /// A representation of a screenshot, as a result of a screenshot captured by the user. /// diff --git a/src/Essentials/src/Screenshot/Screenshot.tizen.cs b/src/Essentials/src/Screenshot/Screenshot.tizen.cs index f7fe990f7908..1bc31dc5446e 100644 --- a/src/Essentials/src/Screenshot/Screenshot.tizen.cs +++ b/src/Essentials/src/Screenshot/Screenshot.tizen.cs @@ -8,7 +8,7 @@ namespace Microsoft.Maui.Media { // TODO: Need to impl - partial class ScreenshotImplementation : IPlatformScreenshot, IScreenshot + partial class ScreenshotImplementation : IPlatformScreenshot, IScreenshot, IViewScreenshot { public bool IsCaptureSupported => throw ExceptionUtils.NotSupportedOrImplementedException; @@ -21,6 +21,11 @@ public Task CaptureAsync(Window window) => public Task CaptureAsync(NView view) => throw ExceptionUtils.NotSupportedOrImplementedException; + +#nullable enable annotations + public Task CaptureViewAsync(object platformView) => + platformView is NView view ? CaptureAsync(view)! : Task.FromResult(null); +#nullable restore } partial class ScreenshotResult diff --git a/src/Essentials/src/Screenshot/Screenshot.windows.cs b/src/Essentials/src/Screenshot/Screenshot.windows.cs index 55e56ba40a8c..4d9072ec62e1 100644 --- a/src/Essentials/src/Screenshot/Screenshot.windows.cs +++ b/src/Essentials/src/Screenshot/Screenshot.windows.cs @@ -11,7 +11,7 @@ namespace Microsoft.Maui.Media { - partial class ScreenshotImplementation : IPlatformScreenshot, IScreenshot + partial class ScreenshotImplementation : IPlatformScreenshot, IScreenshot, IViewScreenshot { public bool IsCaptureSupported => true; @@ -57,6 +57,11 @@ public async Task CaptureAsync(UIElement element) return new ScreenshotResult(width, height, pixels); } } + +#nullable enable annotations + public Task CaptureViewAsync(object platformView) => + platformView is UIElement element ? CaptureAsync(element)! : Task.FromResult(null); +#nullable restore } partial class ScreenshotResult