Skip to content

Commit

Permalink
feat: 2-layer skia for WPF
Browse files Browse the repository at this point in the history
  • Loading branch information
ramezgerges committed May 17, 2024
1 parent 6dc6c24 commit 7caa5d2
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 13 deletions.
32 changes: 31 additions & 1 deletion src/Uno.UI.Runtime.Skia.Wpf/Input/WpfCorePointerInputSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using Uno.Foundation.Logging;
using Uno.UI.Dispatching;
using Uno.UI.Hosting;
Expand All @@ -17,6 +18,8 @@
using Windows.System;
using Windows.UI.Core;
using Windows.UI.Input;
using Uno.UI.Runtime.Skia.Wpf;
using Uno.UI.Runtime.Skia.Wpf.Hosting;
using Point = System.Windows.Point;
using Rect = Windows.Foundation.Rect;
using WpfControl = System.Windows.Controls.Control;
Expand Down Expand Up @@ -110,9 +113,16 @@ public void ReleasePointerCapture(PointerIdentifier pointer)
public void ReleasePointerCapture()
=> _hostControl.ReleaseMouseCapture();

private RoutedEventArgs? _lastArgs;

#region Native events
private void HostOnMouseEvent(InputEventArgs args, TypedEventHandler<object, PointerEventArgs>? @event, [CallerArgumentExpression(nameof(@event))] string eventName = "")
{
if (_lastArgs == args)
{
return;
}
_lastArgs = args;
var current = SynchronizationContext.Current;
try
{
Expand All @@ -123,7 +133,27 @@ private void HostOnMouseEvent(InputEventArgs args, TypedEventHandler<object, Poi
}

var eventArgs = BuildPointerArgs(args);
@event?.Invoke(this, eventArgs);

// Is the pointer inside an element in the flyout layer? if so, raise in managed
var xamlRoot = WpfManager.XamlRootMap.GetRootForHost((IWpfXamlRootHost)_hostControl);
var managedHitTestResult = Microsoft.UI.Xaml.Media.VisualTreeHelper.SearchDownForTopMostElementAt(eventArgs.CurrentPoint.Position, xamlRoot!.VisualTree.PopupRoot!, Microsoft.UI.Xaml.Media.VisualTreeHelper.DefaultGetTestability, null);
if (managedHitTestResult.element is { })
{
@event?.Invoke(this, eventArgs);
}
else
{
// if not, is it on top of a native element? if so, raise in native
var result = VisualTreeHelper.HitTest(((IWpfXamlRootHost)_hostControl).NativeOverlayLayer!, args.GetPosition(null));
if (result?.VisualHit is UIElement element)
{
element.RaiseEvent(args);
}
else // if not, then raise in managed
{
@event?.Invoke(this, eventArgs);
}
}
_previous = eventArgs;
}
catch (Exception e)
Expand Down
10 changes: 8 additions & 2 deletions src/Uno.UI.Runtime.Skia.Wpf/Rendering/OpenGLWpfRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal partial class OpenGLWpfRenderer : IWpfRenderer

private readonly WpfControl _hostControl;
private readonly IWpfXamlRootHost _host;
private readonly bool _isFlyoutSurface;
private readonly WinUI.XamlRoot _xamlRoot;
private nint _hwnd;
private nint _hdc;
Expand All @@ -32,11 +33,16 @@ internal partial class OpenGLWpfRenderer : IWpfRenderer
private GRBackendRenderTarget? _renderTarget;
private WriteableBitmap? _backBuffer;

public OpenGLWpfRenderer(IWpfXamlRootHost host)
public OpenGLWpfRenderer(IWpfXamlRootHost host, bool isFlyoutSurface)
{
_hostControl = host as WpfControl ?? throw new InvalidOperationException("Host should be a WPF control");
_host = host;
_xamlRoot = WpfManager.XamlRootMap.GetRootForHost(host) ?? throw new InvalidOperationException("XamlRoot must not be null when renderer is initialized");
_isFlyoutSurface = isFlyoutSurface;
if (isFlyoutSurface)
{
BackgroundColor = SKColors.Transparent;
}
}

public SKColor BackgroundColor { get; set; }
Expand Down Expand Up @@ -214,7 +220,7 @@ public void Render(DrawingContext drawingContext)

if (_host.RootElement?.Visual is { } rootVisual)
{
rootVisual.Compositor.RenderRootVisual(_surface, rootVisual, false);
rootVisual.Compositor.RenderRootVisual(_surface, rootVisual, _isFlyoutSurface);
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/Uno.UI.Runtime.Skia.Wpf/Rendering/SoftwareWpfRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ internal class SoftwareWpfRenderer : IWpfRenderer
private WriteableBitmap? _bitmap;
private IWpfXamlRootHost _host;
private readonly XamlRoot _xamlRoot;
private bool _isFlyoutSurface;

public SoftwareWpfRenderer(IWpfXamlRootHost host)
public SoftwareWpfRenderer(IWpfXamlRootHost host, bool isFlyoutSurface)
{
_hostControl = host as WpfControl ?? throw new InvalidOperationException("Host should be a WPF control");
_host = host;
_xamlRoot = WpfManager.XamlRootMap.GetRootForHost(host) ?? throw new InvalidOperationException("XamlRoot must not be null when renderer is initialized");
_isFlyoutSurface = isFlyoutSurface;
if (isFlyoutSurface)
{
BackgroundColor = SKColors.Transparent;
}
}

public SKColor BackgroundColor { get; set; } = SKColors.White;
Expand Down Expand Up @@ -83,7 +89,7 @@ public void Render(DrawingContext drawingContext)
surface.Canvas.SetMatrix(SKMatrix.CreateScale((float)dpiScaleX, (float)dpiScaleY));
if (_host.RootElement?.Visual is { } rootVisual)
{
rootVisual.Compositor.RenderRootVisual(surface, rootVisual, false);
rootVisual.Compositor.RenderRootVisual(surface, rootVisual, _isFlyoutSurface);

if (rootVisual.Compositor.IsSoftwareRenderer is null)
{
Expand Down
6 changes: 3 additions & 3 deletions src/Uno.UI.Runtime.Skia.Wpf/Rendering/WpfRendererProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Uno.UI.Runtime.Skia.Wpf.Rendering;

internal static class WpfRendererProvider
{
public static IWpfRenderer CreateForHost(IWpfXamlRootHost host)
public static IWpfRenderer CreateForHost(IWpfXamlRootHost host, bool isFlyoutSurface)
{
var requestedRenderer = host.RenderSurfaceType ?? RenderSurfaceType.OpenGL;

Expand All @@ -20,8 +20,8 @@ public static IWpfRenderer CreateForHost(IWpfXamlRootHost host)
{
renderer = requestedRenderer switch
{
RenderSurfaceType.Software => new SoftwareWpfRenderer(host),
RenderSurfaceType.OpenGL => new OpenGLWpfRenderer(host),
RenderSurfaceType.Software => new SoftwareWpfRenderer(host, isFlyoutSurface),
RenderSurfaceType.OpenGL => new OpenGLWpfRenderer(host, isFlyoutSurface),
_ => throw new InvalidOperationException($"Render Surface type {host.RenderSurfaceType} is not supported")
};

Expand Down
29 changes: 29 additions & 0 deletions src/Uno.UI.Runtime.Skia.Wpf/Themes/Generic.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,35 @@
</Setter>
</Style>

<Style TargetType="{x:Type controls:UnoCompositeWindowHost}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:UnoCompositeWindowHost}">
<Grid>
<Border
Background="{x:Null}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter x:Name="BottomLayerHost" />
</Border>
<Border
Background="{x:Null}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter x:Name="NativeOverlayLayerHost" />
</Border>
<Border
Background="{x:Null}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter x:Name="FlyoutLayerHost" />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style TargetType="{x:Type controls:WpfTextViewTextBox}">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="KeyboardNavigation.TabNavigation" Value="None" />
Expand Down
138 changes: 138 additions & 0 deletions src/Uno.UI.Runtime.Skia.Wpf/UI/Controls/UnoCompositeWindowHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#nullable enable

using SkiaSharp;
using Uno.Disposables;
using Uno.Foundation.Logging;
using Uno.UI.Hosting;
using Uno.UI.Runtime.Skia.Wpf.Extensions;
using Uno.UI.Runtime.Skia.Wpf.Hosting;
using Uno.UI.Runtime.Skia.Wpf.Rendering;
using WpfCanvas = System.Windows.Controls.Canvas;
using WpfContentPresenter = System.Windows.Controls.ContentPresenter;
using WpfControl = System.Windows.Controls.Control;
using WpfFrameworkPropertyMetadata = System.Windows.FrameworkPropertyMetadata;
using MUX = Microsoft.UI.Xaml;

namespace Uno.UI.Runtime.Skia.Wpf.UI.Controls;

internal class UnoCompositeWindowHost : WpfControl, IWpfWindowHost
{
private const string NativeOverlayLayerHost = "NativeOverlayLayerHost";
private const string BottomLayerHost = "BottomLayerHost";
private const string FlyoutLayerHost = "FlyoutLayerHost";

private readonly UnoWpfWindow _wpfWindow;
private readonly MUX.Window _winUIWindow;

private readonly RenderingLayerHost _bottomLayer;
private readonly WpfCanvas _nativeOverlayLayer;
private readonly RenderingLayerHost _flyoutLayer;

private readonly SerialDisposable _backgroundDisposable = new();

static UnoCompositeWindowHost()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(UnoCompositeWindowHost),
new WpfFrameworkPropertyMetadata(typeof(UnoCompositeWindowHost)));
}

public UnoCompositeWindowHost(UnoWpfWindow wpfWindow, MUX.Window winUIWindow)
{
_wpfWindow = wpfWindow;
_winUIWindow = winUIWindow;

// We need to set the content here because the RenderingLayerHost needs to call Window.GetWindow
wpfWindow.Content = this;

FocusVisualStyle = null;

_bottomLayer = new RenderingLayerHost(WpfRendererProvider.CreateForHost(this, false));
_nativeOverlayLayer = new WpfCanvas();
_flyoutLayer = new RenderingLayerHost(WpfRendererProvider.CreateForHost(this, true));

Loaded += WpfHost_Loaded;
Unloaded += (_, _) => _backgroundDisposable.Dispose();

UpdateRendererBackground();
_backgroundDisposable.Disposable = _winUIWindow.RegisterBackgroundChangedEvent((_, _) => UpdateRendererBackground());
}

public WpfControl FlyoutLayer => _flyoutLayer;
public WpfControl BottomLayer => _flyoutLayer;

private void WpfHost_Loaded(object _, System.Windows.RoutedEventArgs __)
{
// Avoid dotted border on focus.
if (Parent is WpfControl control)
{
control.FocusVisualStyle = null;
_bottomLayer.FocusVisualStyle = null;
_flyoutLayer.FocusVisualStyle = null;
}
}

void UpdateRendererBackground()
{
// the flyout layer always has a transparent background so that elements underneath can be seen.
_flyoutLayer.Renderer.BackgroundColor = SKColors.Transparent;

if (_winUIWindow.Background is MUX.Media.SolidColorBrush brush)
{
_bottomLayer.Renderer.BackgroundColor = brush.Color;
_wpfWindow.Background = new System.Windows.Media.SolidColorBrush(brush.Color.ToWpfColor());
}
else
{
if (this.Log().IsEnabled(LogLevel.Warning))
{
this.Log().Warn($"This platform only supports SolidColorBrush for the Window background");
}
}
}

public override void OnApplyTemplate()
{
base.OnApplyTemplate();

if (GetTemplateChild(BottomLayerHost) is WpfContentPresenter bottomLayerHost)
{
bottomLayerHost.Content = _bottomLayer;
}
if (GetTemplateChild(NativeOverlayLayerHost) is WpfContentPresenter nativeOverlayLayerHost)
{
nativeOverlayLayerHost.Content = _nativeOverlayLayer;
}
if (GetTemplateChild(FlyoutLayerHost) is WpfContentPresenter flyoutLayerHost)
{
flyoutLayerHost.Content = _flyoutLayer;
}
}

MUX.UIElement? IXamlRootHost.RootElement => _winUIWindow.RootElement;

void IXamlRootHost.InvalidateRender()
{
_winUIWindow.RootElement?.XamlRoot?.InvalidateOverlays();
_bottomLayer.InvalidateVisual();
_flyoutLayer.InvalidateVisual();
InvalidateVisual();
}

WpfCanvas IWpfXamlRootHost.NativeOverlayLayer => _nativeOverlayLayer;

bool IWpfXamlRootHost.IgnorePixelScaling => WpfHost.Current?.IgnorePixelScaling ?? false;

RenderSurfaceType? IWpfXamlRootHost.RenderSurfaceType => WpfHost.Current?.RenderSurfaceType ?? null;

private class RenderingLayerHost(IWpfRenderer renderer) : WpfControl
{
public IWpfRenderer Renderer { get; } = renderer;

protected override void OnRender(System.Windows.Media.DrawingContext drawingContext)
{
base.OnRender(drawingContext);
Renderer.Render(drawingContext);
}
}
}
8 changes: 5 additions & 3 deletions src/Uno.UI.Runtime.Skia.Wpf/UI/Controls/UnoWpfWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
using Windows.ApplicationModel.Core;
using Uno.Foundation.Logging;
using Windows.UI.ViewManagement;
using Uno.UI.Runtime.Skia.Wpf.Hosting;
using WinUI = Microsoft.UI.Xaml;
using WinUIApplication = Microsoft.UI.Xaml.Application;
using WpfWindow = System.Windows.Window;
using WpfControl = System.Windows.Controls.Control;

namespace Uno.UI.Runtime.Skia.Wpf.UI.Controls;

Expand All @@ -37,8 +39,8 @@ public UnoWpfWindow(WinUI.Window winUIWindow, WinUI.XamlRoot xamlRoot)
Height = (int)preferredWindowSize.Height;
}

Content = Host = new UnoWpfWindowHost(this, winUIWindow);
WpfManager.XamlRootMap.Register(xamlRoot, Host);
Host = new UnoCompositeWindowHost(this, winUIWindow);
WpfManager.XamlRootMap.Register(xamlRoot, (IWpfXamlRootHost)Host);

_applicationView = ApplicationView.GetForWindowId(winUIWindow.AppWindow.Id);
_applicationView.PropertyChanged += OnApplicationViewPropertyChanged;
Expand Down Expand Up @@ -75,7 +77,7 @@ private void UnoWpfWindow_Closed(object? sender, EventArgs e)
_winUIWindow.AppWindow.TitleBar.ExtendsContentIntoTitleBarChanged -= ExtendContentIntoTitleBar;
}

internal UnoWpfWindowHost Host { get; private set; }
internal WpfControl Host { get; }

private void OnApplicationViewPropertyChanged(object? sender, PropertyChangedEventArgs e) => UpdateWindowPropertiesFromApplicationView();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ protected override void OnRender(DrawingContext drawingContext)

if (_renderer is null)
{
_renderer = WpfRendererProvider.CreateForHost(this);
_renderer = WpfRendererProvider.CreateForHost(this, false);
UpdateRendererBackground();
}

Expand Down
2 changes: 1 addition & 1 deletion src/Uno.UI/UI/Xaml/Media/VisualTreeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ internal static (UIElement? element, Branch? stale) HitTest(
/// On skia: The absolute position relative to the window origin.
/// Everywhere else: The position relative to the parent (i.e. the position in parent coordinates).
/// </param>
private static (UIElement? element, Branch? stale) SearchDownForTopMostElementAt(
internal static (UIElement? element, Branch? stale) SearchDownForTopMostElementAt(
Point position,
UIElement element,
GetHitTestability getVisibility,
Expand Down

0 comments on commit 7caa5d2

Please sign in to comment.