From 30ab0b400421f6128c406285386c6e8f99277a99 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Wed, 27 Mar 2024 13:26:31 +0100 Subject: [PATCH] feat: Port RootScale and related classes --- src/Uno.UI/UI/Xaml/Internal/Inlined.cs | 14 ++ src/Uno.UI/UI/Xaml/Internal/RootScale.cs | 27 --- .../Scaling/CoreWindowRootScale.mux.cs | 54 +++++ .../Xaml/Internal/Scaling/RootScale.h.mux.cs | 82 +++++++ .../UI/Xaml/Internal/Scaling/RootScale.mux.cs | 213 ++++++++++++++++++ .../Scaling/XamlIslandRootScale.mux.cs | 66 ++++++ 6 files changed, 429 insertions(+), 27 deletions(-) create mode 100644 src/Uno.UI/UI/Xaml/Internal/Inlined.cs delete mode 100644 src/Uno.UI/UI/Xaml/Internal/RootScale.cs create mode 100644 src/Uno.UI/UI/Xaml/Internal/Scaling/CoreWindowRootScale.mux.cs create mode 100644 src/Uno.UI/UI/Xaml/Internal/Scaling/RootScale.h.mux.cs create mode 100644 src/Uno.UI/UI/Xaml/Internal/Scaling/RootScale.mux.cs create mode 100644 src/Uno.UI/UI/Xaml/Internal/Scaling/XamlIslandRootScale.mux.cs diff --git a/src/Uno.UI/UI/Xaml/Internal/Inlined.cs b/src/Uno.UI/UI/Xaml/Internal/Inlined.cs new file mode 100644 index 000000000000..12c310795653 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/Inlined.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Uno.UI.Xaml.Internal; + +internal static class Inlined +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsCloseReal(float a, float b) => Math.Abs(a - b) < float.Epsilon; // TODO Uno: The original logic is more complex, but might not be necessary in Uno. +} diff --git a/src/Uno.UI/UI/Xaml/Internal/RootScale.cs b/src/Uno.UI/UI/Xaml/Internal/RootScale.cs deleted file mode 100644 index 52a9a8b57dae..000000000000 --- a/src/Uno.UI/UI/Xaml/Internal/RootScale.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable enable - -using Microsoft.UI.Xaml; - -namespace Uno.UI.Xaml.Core -{ - internal static class RootScale - { - [NotImplemented] - internal static float GetRasterizationScaleForContentRoot(ContentRoot? contentRoot) => 1f; - - internal static double GetRasterizationScaleForElement(DependencyObject element) - { - var rootScale = GetRootScaleForElement(element); - return rootScale ?? 1.0d; - } - - internal static double? GetRootScaleForElement(DependencyObject element) - { - if (element is FrameworkElement fe) - { - return fe.GetScaleFactorForLayoutRounding(); - } - return null; - } - } -} diff --git a/src/Uno.UI/UI/Xaml/Internal/Scaling/CoreWindowRootScale.mux.cs b/src/Uno.UI/UI/Xaml/Internal/Scaling/CoreWindowRootScale.mux.cs new file mode 100644 index 000000000000..7d46e36e8876 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/Scaling/CoreWindowRootScale.mux.cs @@ -0,0 +1,54 @@ +namespace Uno.UI.Xaml.Core.Scaling; + +internal class CoreWindowRootScale : RootScale +{ + public CoreWindowRootScale(RootScaleConfig config, CoreServices coreServices, VisualTree visualTree) : + base(config, coreServices, visualTree) + { + } + + protected override void ApplyScaleProtected(bool scaleChanged) + { + //var mainRootVisual = _coreServices.MainRootVisual; + //if (mainRootVisual is not null) + //{ + // float scale = GetRootVisualScale(); + // // The composition subsystem has installed a scale transform on top of our tree, update our tree to be aware of it + // // This ensures that Xaml will know about the physical DPI when needed, e.g. rendering crisp text. + // mainRootVisual.RasterizationScale = scale; + + // //const auto connectedAnimationRoot = m_pCoreServices->GetConnectedAnimationRoot(); + // //if (connectedAnimationRoot) + // //{ + // // // Plateau scale has been applied on the island, and we need to cancel it here, + // // // because the snapshots are created using the pixel size including the plateau scale, + // // // and we don't want double scaling. + // // CValue inverseScaleTransform; + // // IFC_RETURN(CreateReverseTransform(&inverseScaleTransform)); + // // IFC_RETURN(connectedAnimationRoot->SetValueByKnownIndex(KnownPropertyIndex::UIElement_RenderTransform, inverseScaleTransform)); + // //} + //} + + //if (scaleChanged) + //{ + // // We need to force re-layout and re-render only if the scale has actually changed. + // m_pCoreServices->MarkRootScaleTransformDirty(); + + // // Warning: This will not work correctly in the Context of AppWindows + // // Task 18843113: Do not use global MRT resource manager, instead add a new MRT Resource instance on each CRootVisualInstanc + // { + // const unsigned int scalePercentage = XcpRound(GetEffectiveRasterizationScale() * 100.0f); + // // Update the scale factor on the resource manager. + // xref_ptr resourceManager; + // IFC_RETURN(m_pCoreServices->GetResourceManager(resourceManager.ReleaseAndGetAddressOf())); + // IFC_RETURN(resourceManager->SetScaleFactor(scalePercentage)); + + // // Flush the XAML parser cache. If the application to reload XAML after a scale change, we want to + // // re-query MRT for the XAML resource, potentially picking up a new resource for the new scale. + // std::shared_ptr spXamlNodeStreamCacheManager; + // IFC_RETURN(m_pCoreServices->GetXamlNodeStreamCacheManager(spXamlNodeStreamCacheManager)); + // spXamlNodeStreamCacheManager->Flush(); + // } + //} + } +} diff --git a/src/Uno.UI/UI/Xaml/Internal/Scaling/RootScale.h.mux.cs b/src/Uno.UI/UI/Xaml/Internal/Scaling/RootScale.h.mux.cs new file mode 100644 index 000000000000..bdd9a8a8e9c2 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/Scaling/RootScale.h.mux.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// MUX Reference dxaml\xcp\components\scaling\inc\RootScale.h, tag winui3/release/1.5.1 + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.UI.Content; +using Microsoft.UI.Xaml; + +namespace Uno.UI.Xaml.Core.Scaling; + +internal enum RootScaleConfig +{ + // Parent scale is identity or it expects the root visual tree to apply system DPI scale itself. + ParentInvert, + // Parent scale already applies the system DPI scale, so need to apply in the internal root visual tree. + ParentApply, +} + +partial class RootScale +{ + internal bool IsInitialized => _initialized; + + internal static float GetRasterizationScaleForContentRoot(ContentRoot? coreContextRoot) + { + if (GetRootScaleForElement(coreContextRoot) is { } rootScale) + { + return rootScale.GetEffectiveRasterizationScale(); + } + return 1.0f; + } + + internal static float GetRasterizationScaleForElement(DependencyObject pDO) + { + if (GetRootScaleForElement(pDO) is { } rootScale) + { + return rootScale.GetEffectiveRasterizationScale(); + } + return 1.0f; + } + + internal static float GetRasterizationScaleForElementWithFallback(DependencyObject pDO) + { + var rootScale = GetRootScaleForElementWithFallback(pDO); + if (rootScale is not null) + { + return rootScale.GetEffectiveRasterizationScale(); + } + return 1.0f; + } + + protected abstract void ApplyScaleProtected(bool scaleChanged); + + private protected VisualTree? VisualTree => _visualTree; + + internal enum ScaleKind + { + System, + Test, + } + + private readonly RootScaleConfig _config; + // The system DPI, this is accumulated scale, + // tipically this is control by the Display Settings app. + private float _systemScale = 1.0f; + // Used only for testing, it replaces the system DPI scale with a value + // This can only be used when config is RootScaleConfig::ParentInvert + private float _testOverrideScale; + private readonly List _displayListeners = new(); + private readonly VisualTree _visualTree; + private bool _initialized; + private bool _updating; + private ImageReloadManager? _imageReloadManager; + private ContentIsland? _content; // IExpCompositionContent + + protected readonly CoreServices _coreServices; +} diff --git a/src/Uno.UI/UI/Xaml/Internal/Scaling/RootScale.mux.cs b/src/Uno.UI/UI/Xaml/Internal/Scaling/RootScale.mux.cs new file mode 100644 index 000000000000..bf6fda58d38c --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/Scaling/RootScale.mux.cs @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#nullable enable + +using Microsoft.UI.Content; +using Microsoft.UI.Xaml; +using Uno.Disposables; +using static Uno.UI.Xaml.Internal.Inlined; + +namespace Uno.UI.Xaml.Core.Scaling; + +internal partial class RootScale +{ + public RootScale(RootScaleConfig config, CoreServices coreServices, VisualTree visualTree) + { + _config = config; + _visualTree = visualTree; + _coreServices = coreServices; + } + + // TODO Uno: Implement + //~RootScale() + //{ + // // It's OK to still have some displayListeners at this point. When a test is shutting down XAML, we'll + // // destroy this object, but we may still have a CLoadedImageSurface object in this list. Since we're shutting + // // down, there won't be new scale changes anyway. + // _displayListeners.Clear(); + // _imageReloadManager.ClearImages(); + //} + + private float GetSystemScale() + { + float rasterizationScale = 1.0f; + if (_content is not null) + { + // For CoreWindow scenarios, the CompositionContent is also listening for the CoreWindow's closed event. + // CompositionContent will get the notification first and close the entire visual tree, then Xaml will + // exit its message loop and tear down the tree. Since CompositionContent already closed everything, + // Xaml will get lots of RO_E_CLOSED errors. These are all safe to ignore. So tolerate RO_E_CLOSED if + // we're also in the middle of tearing down the tree. + rasterizationScale = _content.RasterizationScale; + } + + return rasterizationScale; + } + + internal void SetContentIsland(ContentIsland? content) => _content = content; + + private void UpdateSystemScale() + { + // Remove SuspendFailFastOnStowedException + // Bug 19696972: QueryScaleFactor silently fails at statup + // SuspendFailFastOnStowedException raiiSuspender; + var systemScale = GetSystemScale(); + if (systemScale != 0.0f) + { + SetSystemScale(systemScale); + } + } + + private float GetEffectiveRasterizationScale() + { + if (!IsInitialized && !_updating) + { + UpdateSystemScale(); + } + + // A testOverrideScale of 0 means there's no override; just use the systemScale + float effectiveScale = _testOverrideScale == 0.0f ? _systemScale : _testOverrideScale; + return effectiveScale; + } + + protected float GetRootVisualScale() + { + // In XamlOneCoreTransforms mode, there is no need to do a RenderTransform on the root, because the scale has already been + // applied for us by the CompositionIsland. However, due to legacy reasons, our DComp tests has a dependency that, even when the scale is 1, + // a RenderTransform is still applied on the root (Identity). To support these tests, we will always apply a scale transform on the root + // in XamlOneCoreTransforms mode. When we've enabled XamlOneCoreTransforms mode by default, we can break this dependency and + // update the tests to not expect an Identity RenderTransform set on the root. + // In OneCoreTransforms mode, there's already a scale applied to XAML visuals matching the systemScale, so we factor that scale + // out on the XAML content. + float newRootVisualScale = 0.0f; + float effectiveScale = GetEffectiveRasterizationScale(); + if (_config == RootScaleConfig.ParentApply) + { + // This is the case where we're pushing a non-identity scale into the root visual + newRootVisualScale = effectiveScale / _systemScale; + } + else + { + newRootVisualScale = effectiveScale; + } + return newRootVisualScale; + } + + private void SetTestOverride(float scale) => SetScale(scale, ScaleKind.Test); + + private void SetSystemScale(float scale) => SetScale(scale, ScaleKind.System); + + private void SetScale(float scale, RootScale.ScaleKind kind) + { + _updating = true; + using var cleanup = Disposable.Create(() => + { + _updating = false; + }); + + float oldScale = GetEffectiveRasterizationScale(); + bool scaleIsValid = scale != 0.0f; + switch (kind) + { + case RootScale.ScaleKind.System: + if (scaleIsValid) + { + _systemScale = scale; + } + break; + case RootScale.ScaleKind.Test: + _testOverrideScale = scale; + break; + } + float newScale = GetEffectiveRasterizationScale(); + bool scaleChanged = !IsCloseReal(oldScale, newScale); + ApplyScale(scaleChanged); + _initialized = true; + } + + private void ApplyScale() => ApplyScale(false); + + private void ApplyScale(bool scaleChanged) + { + ApplyScaleProtected(scaleChanged); + + if (scaleChanged) + { + foreach (var displayListener in _displayListeners) + { + displayListener.OnScaleChanged(); + } + + // TODO Uno: Reload images on scale change! + //if (IsInitialized()) + //{ + // // Reload images. + // m_imageReloadManager.ReloadImages(ResourceInvalidationReason.ScaleChanged); + //} + + VisualTree.ContentRoot.AddPendingXamlRootChangedEvent(ContentRoot.ChangeType.RasterizationScale); + } + } + + private void AddDisplayListener(DisplayListener displayListener) + { + MUX_ASSERT(!_displayListeners.Contains(displayListener)); + _displayListeners.Add(displayListener); + } + + private void RemoveDisplayListener(DisplayListener displayListener) + { + MUX_ASSERT(_displayListeners.Count(d => d == displayListener) == 1); + _displayListeners.Remove(displayListener); + } + + //CImageReloadManager& RootScale::GetImageReloadManager() + //{ + // return m_imageReloadManager; + //} + + private RootScale? GetRootScaleForElement(DependencyObject pDO) + { + if (VisualTree.GetContentRootForElement(pDO) is { } contentRoot) + { + return GetRootScaleForContentRoot(contentRoot); + } + + return null; + } + + private RootScale? GetRootScaleForContentRoot(ContentRoot contentRoot) + { + if (contentRoot is not null) + { + if (contentRoot.VisualTree is { } visualTree) + { + return visualTree.RootScale; + } + } + + return null; + } + + private RootScale? GetRootScaleForElementWithFallback(DependencyObject? pDO) + { + RootScale? result = null; + if (pDO is not null) + { + result = GetRootScaleForElement(pDO); + } + + if (result is null) + { + var coreServices = CoreServices.Instance; // TODO Uno: This should be DXamlServices::GetHandle() + var contentRootCoordinator = coreServices.ContentRootCoordinator; + if (contentRootCoordinator.CoreWindowContentRoot is { } root) + { + result = GetRootScaleForContentRoot(root); + } + } + + return result; + } +} diff --git a/src/Uno.UI/UI/Xaml/Internal/Scaling/XamlIslandRootScale.mux.cs b/src/Uno.UI/UI/Xaml/Internal/Scaling/XamlIslandRootScale.mux.cs new file mode 100644 index 000000000000..8e7f4a5c0e85 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/Scaling/XamlIslandRootScale.mux.cs @@ -0,0 +1,66 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Uno.UI.Xaml.Core.Scaling; + +internal class XamlIslandRootScale : RootScale +{ + public XamlIslandRootScale(RootScaleConfig config, CoreServices coreServices, VisualTree visualTree) : + base(config, coreServices, visualTree) + { + } + + protected override void ApplyScaleProtected(bool scaleChanged) + { + var visualTree = VisualTree; + var rootElement = visualTree.RootElement; + if (rootElement is not null && scaleChanged) + { + // TODO Uno: Uncomment once method is present + //rootElement.SetEntireSubtreeDirty(); + } + + //const auto connectedAnimationRoot = visualTree->GetConnectedAnimationRoot(); + //if (connectedAnimationRoot) + //{ + // // Plateau scale has been applied on the content, and we need to cancel it here, + // // because the snapshots are created using the pixel size including the plateau scale, + // // and we don't want double scaling. + // CValue inverseScaleTransform; + // const float scale = 1.0f / GetSystemScale(); + // CREATEPARAMETERS cp(m_pCoreServices); + // CValue value; + // { + // xref_ptr matrix; + // IFC_RETURN(CMatrix::Create(matrix.ReleaseAndGetAddressOf(), &cp)); + // value.SetFloat(scale); + // IFC_RETURN(matrix.get()->SetValueByKnownIndex(KnownPropertyIndex::Matrix_M11, value)); + // IFC_RETURN(matrix.get()->SetValueByKnownIndex(KnownPropertyIndex::Matrix_M22, value)); + // { + // xref_ptr matrixTransform; + // IFC_RETURN(CMatrixTransform::Create(matrixTransform.ReleaseAndGetAddressOf(), &cp)); + // value.WrapObjectNoRef(matrix.get()); + // IFC_RETURN(matrixTransform.get()->SetValueByKnownIndex(KnownPropertyIndex::MatrixTransform_Matrix, value)); + // inverseScaleTransform.SetObjectAddRef(matrixTransform.get()); + // } + // } + + // IFC_RETURN(connectedAnimationRoot->SetValueByKnownIndex(KnownPropertyIndex::UIElement_RenderTransform, inverseScaleTransform)); + //} + + //if (scaleChanged) + //{ + // int scalePercentage = Math.Round(GetEffectiveRasterizationScale() * 100.0f, 0); + // // Update the scale factor on the resource manager. + // xref_ptr resourceManager; + // IFC_RETURN(m_pCoreServices->GetResourceManager(resourceManager.ReleaseAndGetAddressOf())); + // IFC_RETURN(resourceManager->SetScaleFactor(scalePercentage)); + + // // Flush the XAML parser cache. If the application to reload XAML after a scale change, we want to + // // re-query MRT for the XAML resource, potentially picking up a new resource for the new scale. + // std::shared_ptr spXamlNodeStreamCacheManager; + // IFC_RETURN(m_pCoreServices->GetXamlNodeStreamCacheManager(spXamlNodeStreamCacheManager)); + // spXamlNodeStreamCacheManager->Flush(); + //} + } +}