未验证 提交 47ef5a29 编写于 作者: D David 提交者: GitHub

Merge pull request #12428 from unoplatform/dev/dr/skiaClip

fix(skia): Fix element not clipped when render transformed
......@@ -647,6 +647,10 @@
<Member fullName="Uno.UI.Skia.RenderSurfaceType" reason="Renamed to Uno.UI.Runtime.Skia.Wpf.RenderSurfaceType" />
<Member fullName="Uno.UI.Extensions.KeycodeExtensions" reason="Does not exist in WinAppSDK" />
<!-- BEGIN Skia clipping -->
<Member fullName="System.Numerics.VectorExtensions" reason="Does not exist in WinAppSDK" />
<!-- END Skia clipping -->
</Types>
<Events>
......@@ -1619,6 +1623,33 @@
reason="Parameter-less ctor does not exist in UWP"/>
<!-- END Android PDFDocument -->
<!-- BEGIN Skia clipping -->
<Member fullName="Windows.Foundation.Point Uno.Extensions.Vector2Extensions.ToPoint(System.Numerics.Vector2 v)" reason="Does not exist in WinAppSDK" />
<Member fullName="System.Numerics.Vector2 Uno.Extensions.Vector2Extensions.ToVector(Windows.Foundation.Point p)" reason="Does not exist in WinAppSDK" />
<Member fullName="System.Numerics.Matrix3x2 Windows.UI.Composition.CompositionClip.get_TransformMatrix()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Vector2 Windows.UI.Composition.CompositionClip.get_Scale()" reason="Converted to auto-property" />
<Member fullName="System.Single Windows.UI.Composition.CompositionClip.get_RotationAngle()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Vector2 Windows.UI.Composition.CompositionClip.get_CenterPoint()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Matrix3x2 Windows.UI.Composition.CompositionGradientBrush.get_TransformMatrix()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Vector2 Windows.UI.Composition.CompositionGradientBrush.get_Scale()" reason="Converted to auto-property" />
<Member fullName="System.Single Windows.UI.Composition.CompositionGradientBrush.get_RotationAngle()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Vector2 Windows.UI.Composition.CompositionGradientBrush.get_CenterPoint()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Matrix3x2 Windows.UI.Composition.CompositionShape.get_TransformMatrix()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Vector2 Windows.UI.Composition.CompositionShape.get_Scale()" reason="Converted to auto-property" />
<Member fullName="System.Single Windows.UI.Composition.CompositionShape.get_RotationAngle()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Vector2 Windows.UI.Composition.CompositionShape.get_CenterPoint()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Matrix3x2 Windows.UI.Composition.CompositionSurfaceBrush.get_TransformMatrix()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Vector2 Windows.UI.Composition.CompositionSurfaceBrush.get_Scale()" reason="Converted to auto-property" />
<Member fullName="System.Single Windows.UI.Composition.CompositionSurfaceBrush.get_RotationAngle()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Vector2 Windows.UI.Composition.CompositionSurfaceBrush.get_CenterPoint()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Matrix4x4 Windows.UI.Composition.Visual.get_TransformMatrix()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Vector3 Windows.UI.Composition.Visual.get_CenterPoint()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Vector3 Windows.UI.Composition.Visual.get_Scale()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Vector3 Windows.UI.Composition.Visual.get_RotationAxis()" reason="Converted to auto-property" />
<Member fullName="System.Single Windows.UI.Composition.Visual.get_RotationAngle()" reason="Converted to auto-property" />
<Member fullName="System.Numerics.Quaternion Windows.UI.Composition.Visual.get_Orientation()" reason="Converted to auto-property" />
<!-- END Skia clipping -->
</Methods>
</IgnoreSet>
<!--
......
......@@ -120,11 +120,11 @@ public record struct ExpectedPixels
public ExpectedPixels WithTolerance(PixelTolerance tolerance)
=> this with { Tolerance = tolerance };
public ExpectedPixels WithColorTolerance(byte tolerance)
=> this with { Tolerance = Tolerance.WithColor(tolerance) };
public ExpectedPixels WithColorTolerance(byte tolerance, ColorToleranceKind kind = default)
=> this with { Tolerance = Tolerance.WithColor(tolerance, kind) };
public ExpectedPixels WithPixelTolerance(int x = 0, int y = 0)
=> this with { Tolerance = Tolerance.WithOffset(x, y) };
public ExpectedPixels WithPixelTolerance(int x = 0, int y = 0, LocationToleranceKind kind = default)
=> this with { Tolerance = Tolerance.WithOffset(x, y, kind) };
public ExpectedPixels Or(ExpectedPixels alternative)
=> this with
......
......@@ -9203,6 +9203,6 @@
<Compile Include="$(MSBuildThisFileDirectory)Windows_Data\Pdf\PdfDocumentRenderTest.xaml.cs">
<DependentUpon>PdfDocumentRenderTest.xaml</DependentUpon>
</Compile>
</ItemGroup>
</ItemGroup>
<Import Project="ItemExclusions.props" />
</Project>
\ No newline at end of file
......@@ -299,11 +299,20 @@ namespace UITests.Windows_UI_Xaml_Shapes
{
var fileName = shape.Name + "_" + string.Join("_", alterators.Select(a => a.Id)) + ".png";
var grid = BuildHoriVertTestGridForScreenshot(shape, alterators);
var loaded = new TaskCompletionSource<object>();
grid.SizeChanged += (snd, e) =>
{
if (e.NewSize != default)
{
loaded.SetResult(default);
}
};
_testZone.Child = grid;
await loaded.Task;
await Task.Yield();
var renderer = new RenderTargetBitmap();
await renderer.RenderAsync(grid);
await renderer.RenderAsync(grid, (int)grid.ActualWidth, (int)grid.ActualHeight); // We explicitly set the size to ignore the screen scaling
var file = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
using (var output = await file.OpenAsync(FileAccessMode.ReadWrite))
......@@ -344,7 +353,7 @@ namespace UITests.Windows_UI_Xaml_Shapes
try
{
var elt = await RenderById(test);
await renderer.RenderAsync(elt); ;
await renderer.RenderAsync(elt, (int)elt.ActualWidth, (int)elt.ActualHeight); // We explicitly set the size to ignore the screen scaling
var pixels = await renderer.GetPixelsAsync();
byte[] testResult = default;
......
#nullable enable
using System.Numerics;
using Uno.Extensions;
using Uno.UI.Composition;
namespace Windows.UI.Composition
namespace Windows.UI.Composition;
public partial class CompositionClip : CompositionObject, I2DTransformableObject
{
public partial class CompositionClip : CompositionObject
{
private Matrix3x2 _transformMatrix = Matrix3x2.Identity;
private Vector2 _scale = new Vector2(1, 1);
private float _rotationAngleInDegrees;
private float _rotationAngle;
private Vector2 _offset = Vector2.Zero;
private Vector2 _centerPoint = Vector2.Zero;
private Vector2 _anchorPoint = Vector2.Zero;
private Matrix3x2 _transformMatrix = Matrix3x2.Identity;
private Vector2 _scale = new Vector2(1, 1);
private float _rotationAngle;
private Vector2 _offset = Vector2.Zero;
private Vector2 _centerPoint = Vector2.Zero;
private Vector2 _anchorPoint = Vector2.Zero;
internal CompositionClip(Compositor compositor) : base(compositor)
{
internal CompositionClip(Compositor compositor) : base(compositor)
{
}
}
public Matrix3x2 TransformMatrix
{
get => _transformMatrix;
set => SetProperty(ref _transformMatrix, value);
}
public Matrix3x2 TransformMatrix
{
get => _transformMatrix;
set => SetProperty(ref _transformMatrix, value);
}
public Vector2 Scale
{
get => _scale;
set => SetProperty(ref _scale, value);
}
public Vector2 Scale
{
get => _scale;
set => SetProperty(ref _scale, value);
}
public float RotationAngleInDegrees
{
get => _rotationAngleInDegrees;
set => SetProperty(ref _rotationAngleInDegrees, value);
}
public float RotationAngleInDegrees
{
get => (float)MathEx.ToDegree(_rotationAngle);
set => RotationAngle = (float)MathEx.ToRadians(value);
}
public float RotationAngle
{
get => _rotationAngle;
set => SetProperty(ref _rotationAngle, value);
}
public float RotationAngle
{
get => _rotationAngle;
set => SetProperty(ref _rotationAngle, value);
}
public Vector2 Offset
{
get => _offset;
set => SetProperty(ref _offset, value);
}
public Vector2 Offset
{
get => _offset;
set => SetProperty(ref _offset, value);
}
public Vector2 CenterPoint
{
get => _centerPoint;
set => SetProperty(ref _centerPoint, value);
}
public Vector2 CenterPoint
{
get => _centerPoint;
set => SetProperty(ref _centerPoint, value);
}
public Vector2 AnchorPoint
{
get => _anchorPoint;
set => SetProperty(ref _anchorPoint, value);
}
public Vector2 AnchorPoint
{
get => _anchorPoint;
set => SetProperty(ref _anchorPoint, value);
}
}
#nullable enable
using System;
using System.Linq;
using SkiaSharp;
namespace Windows.UI.Composition;
partial class CompositionClip
{
internal void Apply(SKSurface surface)
{
if (this is InsetClip insetClip)
{
surface.Canvas.ClipRect(insetClip.SKRect, SKClipOperation.Intersect, true);
}
else if (this is RectangleClip rectangleClip)
{
surface.Canvas.ClipRoundRect(rectangleClip.SKRoundRect, SKClipOperation.Intersect, true);
}
else if (this is CompositionGeometricClip geometricClip)
{
switch (geometricClip.Geometry)
{
case CompositionPathGeometry { Path.GeometrySource: SkiaGeometrySource2D geometrySource }:
surface.Canvas.ClipPath(geometrySource.Geometry, antialias: true);
break;
case CompositionPathGeometry cpg:
throw new InvalidOperationException($"Clipping with source {cpg.Path?.GeometrySource} is not supported");
case null:
// null is nop
break;
default:
throw new InvalidOperationException($"Clipping with {geometricClip.Geometry} is not supported");
}
}
}
}
......@@ -2,17 +2,18 @@
using System;
using System.Numerics;
using Uno.Extensions;
using Uno.UI.Composition;
namespace Windows.UI.Composition
{
public partial class CompositionGradientBrush : CompositionBrush
public partial class CompositionGradientBrush : CompositionBrush, I2DTransformableObject
{
private CompositionGradientExtendMode _extendMode;
private CompositionMappingMode _mappingMode;
private Matrix3x2 _transformMatrix = Matrix3x2.Identity;
private Matrix3x2 _relativeTransformMatrix = Matrix3x2.Identity;
private Vector2 _scale = new Vector2(1, 1);
private float _rotationAngleInDegrees;
private float _rotationAngle;
private Vector2 _offset;
private Vector2 _centerPoint;
......@@ -28,13 +29,13 @@ namespace Windows.UI.Composition
public CompositionGradientExtendMode ExtendMode
{
get => _extendMode;
set => SetProperty(ref _extendMode, value);
set => SetEnumProperty(ref _extendMode, value);
}
public CompositionMappingMode MappingMode
{
get => _mappingMode;
set => SetProperty(ref _mappingMode, value);
set => SetEnumProperty(ref _mappingMode, value);
}
public Matrix3x2 TransformMatrix
......@@ -51,22 +52,14 @@ namespace Windows.UI.Composition
public float RotationAngleInDegrees
{
get => _rotationAngleInDegrees;
set
{
_rotationAngle = value * (float)(Math.PI / 180);
SetProperty(ref _rotationAngleInDegrees, value);
}
get => (float)MathEx.ToDegree(_rotationAngle);
set => RotationAngle = (float)MathEx.ToRadians(value);
}
public float RotationAngle
{
get => _rotationAngle;
set
{
_rotationAngleInDegrees = value * 180 / (float)Math.PI;
SetProperty(ref _rotationAngle, value);
}
set => SetProperty(ref _rotationAngle, value);
}
public Vector2 Offset
......
#nullable enable
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using Windows.Foundation.Metadata;
using Windows.UI;
using Windows.UI.Core;
namespace Windows.UI.Composition
......@@ -55,8 +58,151 @@ namespace Windows.UI.Composition
_contextStore.RemoveContext(context, propertyName);
}
private protected void SetProperty(ref bool field, bool value, [CallerMemberName] string? propertyName = null)
{
if (field == value)
{
return;
}
field = value;
OnPropertyChanged(propertyName, false);
}
private protected void SetProperty(ref int field, int value, [CallerMemberName] string? propertyName = null)
{
if (field == value)
{
return;
}
field = value;
OnPropertyChanged(propertyName, false);
}
private protected void SetProperty(ref float field, float value, [CallerMemberName] string? propertyName = null)
{
if (field == value)
{
return;
}
field = value;
OnPropertyChanged(propertyName, false);
}
private protected void SetProperty(ref Matrix3x2 field, Matrix3x2 value, [CallerMemberName] string? propertyName = null)
{
if (field == value)
{
return;
}
field = value;
OnPropertyChanged(propertyName, false);
}
private protected void SetProperty(ref Matrix4x4 field, Matrix4x4 value, [CallerMemberName] string? propertyName = null)
{
if (field == value)
{
return;
}
field = value;
OnPropertyChanged(propertyName, false);
}
private protected void SetProperty(ref Vector2 field, Vector2 value, [CallerMemberName] string? propertyName = null)
{
if (field == value)
{
return;
}
field = value;
OnPropertyChanged(propertyName, false);
}
private protected void SetProperty(ref Vector3 field, Vector3 value, [CallerMemberName] string? propertyName = null)
{
if (field == value)
{
return;
}
field = value;
OnPropertyChanged(propertyName, false);
}
private protected void SetProperty(ref Quaternion field, Quaternion value, [CallerMemberName] string? propertyName = null)
{
if (field == value)
{
return;
}
field = value;
OnPropertyChanged(propertyName, false);
}
private protected void SetProperty(ref Color field, Color value, [CallerMemberName] string? propertyName = null)
{
if (field == value)
{
return;
}
field = value;
OnPropertyChanged(propertyName, false);
}
private protected void SetEnumProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
where T : Enum
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return;
}
field = value;
OnPropertyChanged(propertyName, false);
}
private protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
where T : CompositionObject?
{
if (field == value)
{
return;
}
OnCompositionPropertyChanged(field, value, propertyName);
field = value;
OnPropertyChanged(propertyName, false);
}
private protected void SetObjectProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (field?.Equals(value) ?? value == null)
{
return;
}
// This check is here for backward compatibility
// Is this valid even for non-composition objects like interface?
var fieldCO = field as CompositionObject;
var valueCO = value as CompositionObject;
if (fieldCO != null || value != null)
......@@ -94,7 +240,6 @@ namespace Windows.UI.Composition
private protected virtual void OnPropertyChangedCore(string? propertyName, bool isSubPropertyChange)
{
}
}
}
......@@ -16,7 +16,7 @@ namespace Windows.UI.Composition
public CompositionPath? Path
{
get => _path;
set => SetProperty(ref _path, value);
set => SetObjectProperty(ref _path, value);
}
internal override IGeometrySource2D? BuildGeometry() => Path?.GeometrySource;
......
......@@ -2,58 +2,58 @@
using System;
using System.Numerics;
using Uno.Extensions;
using Uno.UI.Composition;
namespace Windows.UI.Composition
namespace Windows.UI.Composition;
public partial class CompositionShape : CompositionObject, I2DTransformableObject
{
public partial class CompositionShape : CompositionObject
private Matrix3x2 _transformMatrix = Matrix3x2.Identity;
private Vector2 _scale = new Vector2(1, 1);
private float _rotationAngle;
private Vector2 _offset;
private Vector2 _centerPoint;
internal CompositionShape() => throw new InvalidOperationException($"Use the Compositor ctor");
internal CompositionShape(Compositor compositor) : base(compositor)
{
}
public Matrix3x2 TransformMatrix
{
get => _transformMatrix;
set => SetProperty(ref _transformMatrix, value);
}
public Vector2 Scale
{
get => _scale;
set => SetProperty(ref _scale, value);
}
public float RotationAngleInDegrees
{
get => (float)MathEx.ToDegree(_rotationAngle);
set => RotationAngle = (float)MathEx.ToRadians(value);
}
public float RotationAngle
{
get => _rotationAngle;
set => SetProperty(ref _rotationAngle, value);
}
public Vector2 Offset
{
get => _offset;
set => SetProperty(ref _offset, value);
}
public Vector2 CenterPoint
{
private Matrix3x2 _transformMatrix = Matrix3x2.Identity;
private Vector2 _scale = new Vector2(1, 1);
private float _rotationAngleInDegrees;
private float _rotationAngle;
private Vector2 _offset;
private Vector2 _centerPoint;
internal CompositionShape() => throw new InvalidOperationException($"Use the Compositor ctor");
internal CompositionShape(Compositor compositor) : base(compositor)
{
}
public Matrix3x2 TransformMatrix
{
get => _transformMatrix;
set => SetProperty(ref _transformMatrix, value);
}
public Vector2 Scale
{
get => _scale;
set => SetProperty(ref _scale, value);
}
public float RotationAngleInDegrees
{
get => _rotationAngleInDegrees;
set => SetProperty(ref _rotationAngleInDegrees, value);
}
public float RotationAngle
{
get => _rotationAngle;
set => SetProperty(ref _rotationAngle, value);
}
public Vector2 Offset
{
get => _offset;
set => SetProperty(ref _offset, value);
}
public Vector2 CenterPoint
{
get => _centerPoint;
set => SetProperty(ref _centerPoint, value);
}
get => _centerPoint;
set => SetProperty(ref _centerPoint, value);
}
}
......@@ -3,14 +3,47 @@
using SkiaSharp;
using System;
using System.Numerics;
using Uno.Extensions;
using Uno.UI.Composition;
namespace Windows.UI.Composition
namespace Windows.UI.Composition;
public partial class CompositionShape
{
public partial class CompositionShape
internal virtual void Render(in DrawingSession session)
{
using var localSession = BeginDrawing(in session);
Draw(in session); // We use the session on purpose here!
}
internal virtual void Draw(in DrawingSession session)
{
}
private DrawingSession? BeginDrawing(in DrawingSession session)
{
internal virtual void Render(SKSurface surface)
var offset = Offset;
var transform = this.GetTransform();
if (offset == Vector2.Zero && transform is { IsIdentity: true })
{
return default; // Use the session without saving it, nothing to dispose
}
session.Surface.Canvas.Save();
if (offset != Vector2.Zero)
{
session.Surface.Canvas.Translate(offset.X, offset.Y);
}
if (transform is { IsIdentity: false })
{
var skTransform = transform.ToSKMatrix();
session.Surface.Canvas.Concat(ref skTransform);
}
return session;
}
}
......@@ -31,7 +31,7 @@ namespace Windows.UI.Composition
public CompositionStrokeCap StrokeStartCap
{
get => _strokeStartCap;
set => SetProperty(ref _strokeStartCap, value);
set => SetEnumProperty(ref _strokeStartCap, value);
}
public float StrokeMiterLimit
......@@ -43,13 +43,13 @@ namespace Windows.UI.Composition
public CompositionStrokeLineJoin StrokeLineJoin
{
get => _strokeLineJoin;
set => SetProperty(ref _strokeLineJoin, value);
set => SetEnumProperty(ref _strokeLineJoin, value);
}
public CompositionStrokeCap StrokeEndCap
{
get => _strokeEndCap;
set => SetProperty(ref _strokeEndCap, value);
set => SetEnumProperty(ref _strokeEndCap, value);
}
public float StrokeDashOffset
......@@ -61,7 +61,7 @@ namespace Windows.UI.Composition
public CompositionStrokeCap StrokeDashCap
{
get => _strokeDashCap;
set => SetProperty(ref _strokeDashCap, value);
set => SetEnumProperty(ref _strokeDashCap, value);
}
public CompositionBrush? StrokeBrush
......
#nullable enable
using SkiaSharp;
using Uno.UI.Composition;
namespace Windows.UI.Composition
{
......@@ -9,51 +10,47 @@ namespace Windows.UI.Composition
private SKPaint? _strokePaint;
private SKPaint? _fillPaint;
internal override void Render(SKSurface surface)
internal override void Draw(in DrawingSession session)
{
SkiaGeometrySource2D? geometrySource = Geometry?.BuildGeometry() as SkiaGeometrySource2D;
SKPath? geometry = geometrySource?.Geometry;
if (geometry == null)
if (Geometry?.BuildGeometry() is SkiaGeometrySource2D { Geometry: { } geometry })
{
return;
}
if (FillBrush is { } fill)
{
var fillPaint = TryCreateAndClearFillPaint(in session);
if (FillBrush != null)
{
var fillPaint = TryCreateAndClearFillPaint();
fill.UpdatePaint(fillPaint, geometry.Bounds);
FillBrush.UpdatePaint(fillPaint, geometry.Bounds);
session.Surface.Canvas.DrawPath(geometry, fillPaint);
}
surface.Canvas.DrawPath(geometry, fillPaint);
}
if (StrokeBrush is { } stroke && StrokeThickness > 0)
{
var fillPaint = TryCreateAndClearFillPaint(in session);
var strokePaint = TryCreateAndClearStrokePaint(in session);
if (StrokeBrush != null && StrokeThickness > 0)
{
var fillPaint = TryCreateAndClearFillPaint();
var strokePaint = TryCreateAndClearStrokePaint();
// Set stroke thickness
strokePaint.StrokeWidth = StrokeThickness;
// TODO: Add support for dashes here
// strokePaint.PathEffect = SKPathEffect.CreateDash();
// Set stroke thickness
strokePaint.StrokeWidth = StrokeThickness;
// TODO: Add support for dashes here
// strokePaint.PathEffect = SKPathEffect.CreateDash();
// Generate stroke geometry for bounds that will be passed to a brush.
// - [Future]: This generated geometry should also be used for hit testing.
using var strokeGeometry = strokePaint.GetFillPath(geometry);
// Generate stroke geometry for bounds that will be passed to a brush.
// - [Future]: This generated geometry should also be used for hit testing.
using (var strokeGeometry = strokePaint.GetFillPath(geometry))
{
StrokeBrush.UpdatePaint(fillPaint, strokeGeometry.Bounds);
stroke.UpdatePaint(fillPaint, strokeGeometry.Bounds);
surface.Canvas.DrawPath(strokeGeometry, fillPaint);
session.Surface.Canvas.DrawPath(strokeGeometry, fillPaint);
}
}
}
private SKPaint TryCreateAndClearStrokePaint() => TryCreateAndClearPaint(ref _strokePaint, true);
private SKPaint TryCreateAndClearStrokePaint(in DrawingSession session)
=> TryCreateAndClearPaint(in session, ref _strokePaint, true);
private SKPaint TryCreateAndClearFillPaint() => TryCreateAndClearPaint(ref _fillPaint, false);
private SKPaint TryCreateAndClearFillPaint(in DrawingSession session)
=> TryCreateAndClearPaint(in session, ref _fillPaint, false);
private SKPaint TryCreateAndClearPaint(ref SKPaint? paint, bool isStroke)
private static SKPaint TryCreateAndClearPaint(in DrawingSession session, ref SKPaint? paint, bool isStroke)
{
if (paint == null)
{
......@@ -76,7 +73,7 @@ namespace Windows.UI.Composition
}
}
paint.ColorFilter = Compositor.CurrentOpacityColorFilter;
paint.ColorFilter = session.Filters.OpacityColorFilter;
return paint;
}
......
#nullable enable
using System.Numerics;
using Uno.Extensions;
using Uno.UI.Composition;
namespace Windows.UI.Composition
{
public partial class CompositionSurfaceBrush : CompositionBrush
public partial class CompositionSurfaceBrush : CompositionBrush, I2DTransformableObject
{
private Matrix3x2 _transformMatrix = Matrix3x2.Identity;
private Vector2 _scale;
private float _rotationAngleInDegrees;
private float _rotationAngle;
private Vector2 _offset;
private Vector2 _centerPoint;
......@@ -38,13 +39,13 @@ namespace Windows.UI.Composition
public ICompositionSurface? Surface
{
get => _surface;
set => SetProperty(ref _surface, value);
set => SetObjectProperty(ref _surface, value);
}
public CompositionStretch Stretch
{
get => _stretch;
set => SetProperty(ref _stretch, value);
set => SetEnumProperty(ref _stretch, value);
}
public float HorizontalAlignmentRatio
......@@ -56,7 +57,7 @@ namespace Windows.UI.Composition
public CompositionBitmapInterpolationMode BitmapInterpolationMode
{
get => _bitmapInterpolationMode;
set => SetProperty(ref _bitmapInterpolationMode, value);
set => SetEnumProperty(ref _bitmapInterpolationMode, value);
}
public Matrix3x2 TransformMatrix
......@@ -73,8 +74,8 @@ namespace Windows.UI.Composition
public float RotationAngleInDegrees
{
get => _rotationAngleInDegrees;
set => SetProperty(ref _rotationAngleInDegrees, value);
get => (float)MathEx.ToDegree(_rotationAngle);
set => RotationAngle = (float)MathEx.ToRadians(value);
}
public float RotationAngle
......
......@@ -2,49 +2,47 @@
using System.Numerics;
namespace Windows.UI.Composition
namespace Windows.UI.Composition;
public partial class CompositionViewBox : CompositionObject
{
public partial class CompositionViewBox : CompositionObject
private float _verticalAlignmentRatio;
private CompositionStretch _stretch;
private Vector2 _size;
private Vector2 _offset;
private float _horizontalAlignmentRatio;
internal CompositionViewBox(Compositor compositor) : base(compositor)
{
}
public float VerticalAlignmentRatio
{
get => _verticalAlignmentRatio;
set => SetProperty(ref _verticalAlignmentRatio, value);
}
public CompositionStretch Stretch
{
get => _stretch;
set => SetEnumProperty(ref _stretch, value);
}
public Vector2 Size
{
get => _size;
set => SetProperty(ref _size, value);
}
public Vector2 Offset
{
get => _offset;
set => SetProperty(ref _offset, value);
}
public float HorizontalAlignmentRatio
{
private float _verticalAlignmentRatio;
private CompositionStretch _stretch;
private Vector2 _size;
private Vector2 _offset;
private float _horizontalAlignmentRatio;
internal CompositionViewBox(Compositor compositor) : base(compositor)
{
}
public float VerticalAlignmentRatio
{
get => _verticalAlignmentRatio;
set => SetProperty(ref _verticalAlignmentRatio, value);
}
public CompositionStretch Stretch
{
get => _stretch;
set => SetProperty(ref _stretch, value);
}
public Vector2 Size
{
get => _size;
set => SetProperty(ref _size, value);
}
public Vector2 Offset
{
get => _offset;
set => SetProperty(ref _offset, value);
}
public float HorizontalAlignmentRatio
{
get => _horizontalAlignmentRatio;
set => SetProperty(ref _horizontalAlignmentRatio, value);
}
get => _horizontalAlignmentRatio;
set => SetProperty(ref _horizontalAlignmentRatio, value);
}
}
#nullable enable
using System;
using System.Linq;
using SkiaSharp;
namespace Windows.UI.Composition;
public partial class CompositionViewBox
{
public SKRect GetRect()
=> new(Offset.X, Offset.Y, Offset.X + Size.X, Offset.Y + Size.Y);
}
......@@ -8,19 +8,13 @@ namespace Windows.UI.Composition
public partial class Compositor : global::System.IDisposable
{
public ContainerVisual CreateContainerVisual()
=> new ContainerVisual(this)
{
};
=> new ContainerVisual(this);
public SpriteVisual CreateSpriteVisual()
=> new SpriteVisual(this)
{
};
=> new SpriteVisual(this);
public CompositionColorBrush CreateColorBrush()
=> new CompositionColorBrush(this)
{
};
=> new CompositionColorBrush(this);
public CompositionColorBrush CreateColorBrush(Color color)
=> new CompositionColorBrush(this)
......@@ -137,6 +131,9 @@ namespace Windows.UI.Composition
Color = color
};
public CompositionViewBox CreateViewBox()
=> new CompositionViewBox(this);
internal void InvalidateRender() => InvalidateRenderPartial();
partial void InvalidateRenderPartial();
......
......@@ -6,172 +6,38 @@ using System.Numerics;
using SkiaSharp;
using Windows.ApplicationModel.Core;
using Windows.UI.Core;
using Uno.Extensions;
namespace Windows.UI.Composition
{
public partial class Compositor
{
private readonly Stack<float> _opacityStack = new Stack<float>();
private float _currentOpacity = 1.0f;
private bool _isDirty;
private SKColorFilter? _currentOpacityColorFilter;
private OpacityDisposable PushOpacity(float opacity)
{
_opacityStack.Push(_currentOpacity);
_currentOpacity *= opacity;
_currentOpacityColorFilter = null;
return new OpacityDisposable(this);
}
private struct OpacityDisposable : IDisposable
{
private readonly Compositor Compositor;
public OpacityDisposable(Compositor compositor)
{
Compositor = compositor;
}
namespace Windows.UI.Composition;
public void Dispose()
{
Compositor._currentOpacity = Compositor._opacityStack.Pop();
Compositor._currentOpacityColorFilter?.Dispose();
Compositor._currentOpacityColorFilter = null;
}
}
internal float CurrentOpacity => _currentOpacity;
internal SKColorFilter? CurrentOpacityColorFilter
{
get
{
if (_currentOpacity != 1.0f)
{
if (_currentOpacityColorFilter is null)
{
var opacity = 255 * _currentOpacity;
_currentOpacityColorFilter = SKColorFilter.CreateBlendMode(new SKColor(0xFF, 0xFF, 0xFF, (byte)opacity), SKBlendMode.Modulate);
}
return _currentOpacityColorFilter;
}
else
{
return null;
}
}
}
public partial class Compositor
{
private bool _isDirty;
internal void RenderRootVisual(SKSurface surface, ContainerVisual rootVisual)
internal void RenderRootVisual(SKSurface surface, ContainerVisual rootVisual)
{
if (rootVisual is null)
{
if (rootVisual is null)
{
throw new ArgumentNullException(nameof(rootVisual));
}
_isDirty = false;
var children = rootVisual.GetChildrenInRenderOrder();
for (var i = 0; i < children.Count; i++)
{
RenderVisual(surface, children[i]);
}
throw new ArgumentNullException(nameof(rootVisual));
}
internal void RenderVisual(SKSurface surface, Visual visual)
{
if (visual.Opacity != 0 && visual.IsVisible)
{
if (visual.ShadowState is { } shadow)
{
surface.Canvas.SaveLayer(shadow.Paint);
}
else
{
surface.Canvas.Save();
}
var visualMatrix = surface.Canvas.TotalMatrix;
visualMatrix = visualMatrix.PreConcat(SKMatrix.CreateTranslation(visual.Offset.X, visual.Offset.Y));
visualMatrix = visualMatrix.PreConcat(SKMatrix.CreateTranslation(visual.AnchorPoint.X, visual.AnchorPoint.Y));
if (visual.RotationAngleInDegrees != 0)
{
visualMatrix = visualMatrix.PreConcat(SKMatrix.CreateRotationDegrees(visual.RotationAngleInDegrees, visual.CenterPoint.X, visual.CenterPoint.Y));
}
if (visual.TransformMatrix != Matrix4x4.Identity)
{
visualMatrix = visualMatrix.PreConcat(visual.TransformMatrix.ToSKMatrix44().Matrix);
}
surface.Canvas.SetMatrix(visualMatrix);
ApplyClip(surface, visual);
_isDirty = false;
using var opacityDisposable = PushOpacity(visual.Opacity);
visual.Render(surface);
surface.Canvas.Restore();
}
}
private static void ApplyClip(SKSurface surface, Visual visual)
// TODO: Why are we enumerating children manually instead of just let the ContainerVisual do its job?
var children = rootVisual.GetChildrenInRenderOrder();
for (var i = 0; i < children.Count; i++)
{
if (visual.Clip is InsetClip insetClip)
{
var clipRect = new SKRect
{
Top = insetClip.TopInset - 1,
Bottom = insetClip.BottomInset + 1,
Left = insetClip.LeftInset - 1,
Right = insetClip.RightInset + 1
};
surface.Canvas.ClipRect(clipRect, SKClipOperation.Intersect, true);
}
else if (visual.Clip is RectangleClip rectangleClip)
{
surface.Canvas.ClipRoundRect(rectangleClip.SKRoundRect, SKClipOperation.Intersect, true);
}
else if (visual.Clip is CompositionGeometricClip geometricClip)
{
if (geometricClip.Geometry is CompositionPathGeometry cpg)
{
if (cpg.Path?.GeometrySource is SkiaGeometrySource2D geometrySource)
{
surface.Canvas.ClipPath(geometrySource.Geometry, antialias: true);
}
else
{
throw new InvalidOperationException($"Clipping with source {cpg.Path?.GeometrySource} is not supported");
}
}
else if (geometricClip.Geometry is null)
{
// null is nop
}
else
{
throw new InvalidOperationException($"Clipping with {geometricClip.Geometry} is not supported");
}
}
children[i].RenderRootVisual(surface);
}
}
partial void InvalidateRenderPartial()
partial void InvalidateRenderPartial()
{
if (!_isDirty)
{
if (!_isDirty)
{
_isDirty = true;
// TODO: Adjust for multi window #8341
CoreApplication.QueueInvalidateRender();
}
_isDirty = true;
// TODO: Adjust for multi window #8341
CoreApplication.QueueInvalidateRender();
}
}
}
......@@ -2,6 +2,7 @@
using SkiaSharp;
using System.Collections.Generic;
using System.Linq;
using Uno.UI.Composition;
namespace Windows.UI.Composition;
......@@ -44,15 +45,15 @@ public partial class ContainerVisual : Visual
IsChildrenRenderOrderDirty = false;
}
internal override void Render(SKSurface surface)
private protected override void Draw(in DrawingSession session)
{
var compositor = this.Compositor;
base.Draw(in session);
var children = GetChildrenInRenderOrder();
var childrenCount = children.Count;
for (int i = 0; i < childrenCount; i++)
for (var i = 0; i < childrenCount; i++)
{
compositor.RenderVisual(surface, children[i]);
children[i].Render(in session);
}
}
}
#nullable enable
using System;
using System.Linq;
using SkiaSharp;
namespace Windows.UI.Composition;
partial class InsetClip
{
internal SKRect SKRect => new()
{
Top = TopInset - 1,
Bottom = BottomInset + 1,
Left = LeftInset - 1,
Right = RightInset + 1
};
}
#nullable enable
namespace Windows.UI.Composition
namespace Windows.UI.Composition;
public partial class ShapeVisual : ContainerVisual
{
public partial class ShapeVisual : ContainerVisual
private CompositionViewBox? _viewBox;
private CompositionShapeCollection? _shapes;
public ShapeVisual(Compositor compositor)
: base(compositor)
{
}
public CompositionViewBox? ViewBox
{
private CompositionViewBox? _viewBox;
get => _viewBox;
set => SetProperty(ref _viewBox, value);
}
public ShapeVisual(Compositor compositor)
: base(compositor)
// This is lazy as we are using the `ShapeVisual` for UIElement, but lot of them are not creating shapes, reduce memory pressure.
public CompositionShapeCollection Shapes
{
get
{
Shapes = new CompositionShapeCollection(compositor, this);
if (_shapes is null)
{
_shapes = new CompositionShapeCollection(Compositor, this);
// Add this as context for the shape collection so we get
// notified about changes in the shapes object graph.
OnCompositionPropertyChanged(null, Shapes, nameof(Shapes));
}
// Add this as context for the shape collection so we get
// notified about changes in the shapes object graph.
OnCompositionPropertyChanged(null, _shapes, nameof(Shapes));
}
public CompositionViewBox? ViewBox
{
get => _viewBox;
set => SetProperty(ref _viewBox, value);
return _shapes;
}
public CompositionShapeCollection Shapes { get; }
}
}
#nullable enable
using System.Numerics;
using Windows.Foundation;
using SkiaSharp;
using Uno.Extensions;
using System.Xml.Xsl;
using Uno.UI.Composition;
namespace Windows.UI.Composition
namespace Windows.UI.Composition;
public partial class ShapeVisual
{
public partial class ShapeVisual
internal override void Render(in DrawingSession parentSession)
{
internal override void Render(SKSurface surface)
if (this is { Opacity: 0 } or { IsVisible: false })
{
foreach (var shape in Shapes)
{
surface.Canvas.Save();
return;
}
var visualMatrix = surface.Canvas.TotalMatrix;
// First we render the shapes (a.k.a. the "local content")
// For UIElement, those are background and border or shape's content
// WARNING: As we are overriding the "Render" method, at this point we are still in the parent's coordinate system
if (_shapes is { Count: not 0 } shapes)
{
using var session = BeginShapesDrawing(in parentSession);
visualMatrix = visualMatrix.PreConcat(SKMatrix.CreateTranslation(shape.Offset.X, shape.Offset.Y));
for (var i = 0; i < shapes.Count; i++)
{
shapes[i].Render(in session);
}
}
if (shape.Scale != new Vector2(1, 1))
{
visualMatrix = visualMatrix.PreConcat(SKMatrix.CreateScale(shape.Scale.X, shape.Scale.Y));
}
// Second we render the children
base.Render(in parentSession);
}
if (shape.RotationAngleInDegrees != 0)
{
visualMatrix = visualMatrix.PreConcat(SKMatrix.CreateRotationDegrees(shape.RotationAngleInDegrees, shape.CenterPoint.X, shape.CenterPoint.Y));
}
/// <inheritdoc />
private protected override void Draw(in DrawingSession session)
{
if (ViewBox is { } viewBox)
{
session.Surface.Canvas.ClipRect(viewBox.GetRect(), antialias: true);
}
if (shape.TransformMatrix != Matrix3x2.Identity)
{
visualMatrix = visualMatrix.PreConcat(shape.TransformMatrix.ToSKMatrix44().Matrix);
}
base.Draw(in session);
}
surface.Canvas.SetMatrix(visualMatrix);
private DrawingSession BeginShapesDrawing(in DrawingSession parentSession)
{
parentSession.Surface.Canvas.Save();
shape.Render(surface);
// Set the position of the visual on the canvas (i.e. change coordinates system to the "XAML element" one)
parentSession.Surface.Canvas.Translate(Offset.X + AnchorPoint.X, Offset.Y + AnchorPoint.Y);
surface.Canvas.Restore();
var transform = this.GetTransform().ToSKMatrix();
if (ViewBox is { } viewBox)
{
// We apply the transformed viewbox clipping
if (transform.IsIdentity)
{
parentSession.Surface.Canvas.ClipRect(viewBox.GetRect(), antialias: true);
}
else
{
var shape = new SKPath();
// Note: Here we apply the view box at 0,0 instead of offset
// This is because the view box is somehow the clipping applied on us by our parent (in its coordinate space),
// but when transformed our shapes can draw their content out that bounds ... but still have to respect the clipping of our parent itself.
shape.AddRect(new SKRect(0, 0, viewBox.Offset.X + viewBox.Size.X, viewBox.Offset.Y + viewBox.Size.Y));
shape.Transform(transform);
parentSession.Surface.Canvas.ClipPath(shape, antialias: true);
}
}
if (!transform.IsIdentity)
{
// Applied rending transformation matrix (i.e. change coordinates system to the "rendering" one)
parentSession.Surface.Canvas.Concat(ref transform);
}
// Note: We don't apply the clip here, as it is already applied on the shapes (i.e. CornerRadius)
// The Clip property is only used to apply the clip on the children (i.e. the UIElement's content)
// Clip?.Apply(parentSession.Surface);
var session = parentSession; // Creates a new session (clone the struct)
DrawingSession.PushOpacity(ref session, Opacity);
return session;
}
}
......@@ -98,6 +98,12 @@ namespace Windows.UI.Composition
return ret;
}
public static SKMatrix ToSKMatrix(this Matrix4x4 m)
=> new(
m.M11, m.M21, m.M41,
m.M12, m.M22, m.M42,
m.M14, m.M24, m.M44);
/// <summary>
/// This is an alternative to the built-in SKBitmap.FromImage.
/// The problem with SKBitmap.FromImage is that it ignores the color type of the input image, and
......
#nullable enable
using SkiaSharp;
using Uno.UI.Composition;
namespace Windows.UI.Composition
{
......@@ -22,27 +23,16 @@ namespace Windows.UI.Composition
Brush?.UpdatePaint(_paint, new SKRect(left: 0, top: 0, right: Size.X, bottom: Size.Y));
}
internal override void Render(SKSurface surface)
private protected override void Draw(in DrawingSession session)
{
base.Render(surface);
base.Draw(in session);
surface.Canvas.Save();
_paint.ColorFilter = session.Filters.OpacityColorFilter;
if (Compositor.CurrentOpacity != 1.0f)
{
_paint.ColorFilter = Compositor.CurrentOpacityColorFilter;
}
else
{
_paint.ColorFilter = null;
}
surface.Canvas.DrawRect(
session.Surface.Canvas.DrawRect(
new SKRect(left: 0, top: 0, right: Size.X, bottom: Size.Y),
_paint
);
surface.Canvas.Restore();
}
}
}
#nullable enable
using System;
using System.Linq;
using SkiaSharp;
namespace Uno.UI.Composition;
/// <summary>
/// A holding struct for effects that should be applied by children instead of visual itself, usually only on actual drawing instead of container visual.
/// </summary>
/// <param name="Opacity">
/// The opacity to apply to children.
/// This should be applied only on drawing and not the container to avoid fade-ou of child containers.
/// </param>
internal record struct DrawingFilters(float Opacity)
{
public static DrawingFilters Default { get; } = new(1.0f);
private SKColorFilter? _opacityColorFilter = null;
// Note: This SKColorFilter might be created more than once since this Filter is a struct.
// However since this Filter is copied (pushed to the stack) only when something changes, it should still catch most cases.
public SKColorFilter? OpacityColorFilter => Opacity is 1.0f
? null
: _opacityColorFilter ??= SKColorFilter.CreateBlendMode(new SKColor(0xFF, 0xFF, 0xFF, (byte)(255 * Opacity)), SKBlendMode.Modulate);
}
#nullable enable
using System;
using System.Linq;
using Windows.UI.Composition;
using SkiaSharp;
namespace Uno.UI.Composition;
internal record struct DrawingSession(SKSurface Surface, in DrawingFilters Filters) : IDisposable
{
public static void PushOpacity(ref DrawingSession session, float opacity)
{
// We try to keep the filter ref as long as possible in order to share the same filter.OpacityColorFilter
if (opacity is not 1.0f)
{
var filters = session.Filters;
session = session with { Filters = filters with { Opacity = filters.Opacity * opacity } };
}
}
/// <inheritdoc />
public void Dispose()
=> Surface.Canvas.Restore();
}
#nullable enable
using System;
using System.Linq;
using System.Numerics;
namespace Uno.UI.Composition;
/// <summary>
/// A composition object that can be transformed in 2D.
/// </summary>
internal interface I2DTransformableObject
{
Matrix3x2 TransformMatrix { get; }
Vector2 Scale { get; }
float RotationAngle { get; }
Vector2 CenterPoint { get; }
}
#nullable enable
using System;
using System.Linq;
using System.Numerics;
namespace Uno.UI.Composition;
/// <summary>
/// A composition object that can be transformed in 3D.
/// </summary>
internal interface I3DTransformableObject
{
Matrix4x4 TransformMatrix { get; }
Vector3 CenterPoint { get; }
Vector3 Scale { get; }
Quaternion Orientation { get; }
float RotationAngle { get; }
Vector3 RotationAxis { get; }
}
#nullable enable
using System;
using System.Linq;
using System.Numerics;
using Uno.UI.Composition;
namespace Uno.Extensions;
internal static class TransformableObjectExtensions
{
/// <summary>
/// Gets the total transformation matrix applied on the object.
/// The resulting matrix only contains transformation related properties, it does **not** include the Offset.
/// </summary>
public static Matrix3x2 GetTransform(this I2DTransformableObject transformableObject)
{
var transform = transformableObject.TransformMatrix;
if (transformableObject.Scale != Vector2.One)
{
transform *= Matrix3x2.CreateScale(transformableObject.Scale, transformableObject.CenterPoint);
}
if (transformableObject.RotationAngle is not 0)
{
transform *= Matrix3x2.CreateRotation(transformableObject.RotationAngle, transformableObject.CenterPoint);
}
return transform;
}
/// <summary>
/// Gets the total transformation matrix applied on the object.
/// The resulting matrix only contains transformation related properties, it does **not** include the Offset.
/// </summary>
public static Matrix4x4 GetTransform(this I3DTransformableObject transformableObject)
{
var transform = transformableObject.TransformMatrix;
var scale = transformableObject.Scale;
if (scale != Vector3.One)
{
transform *= Matrix4x4.CreateScale(scale, transformableObject.CenterPoint);
}
var orientation = transformableObject.Orientation;
if (orientation != Quaternion.Identity)
{
transform *= Matrix4x4.CreateFromQuaternion(orientation);
}
var rotation = transformableObject.RotationAngle;
if (rotation is not 0)
{
transform *= Matrix4x4.CreateFromAxisAngle(transformableObject.RotationAxis, rotation);
}
return transform;
}
}
#nullable enable
using System.Numerics;
using Uno.Extensions;
using Uno.UI.Composition;
namespace Windows.UI.Composition
{
public partial class Visual : CompositionObject
public partial class Visual : CompositionObject, I3DTransformableObject
{
private Vector2 _size;
private Vector3 _offset;
private Vector3 _scale = new Vector3(1, 1, 1);
private Vector3 _centerPoint;
private float _rotationAngleInDegrees;
private Quaternion _orientation = Quaternion.Identity;
private float _rotationAngle;
private Vector3 _rotationAxis = new Vector3(0, 0, 1);
private Matrix4x4 _transformMatrix = Matrix4x4.Identity;
private bool _isVisible = true;
......@@ -47,7 +50,7 @@ namespace Windows.UI.Composition
public CompositionCompositeMode CompositeMode
{
get => _compositeMode;
set => SetProperty(ref _compositeMode, value);
set => SetEnumProperty(ref _compositeMode, value);
}
public Vector3 CenterPoint
......@@ -66,13 +69,27 @@ namespace Windows.UI.Composition
partial void OnScaleChanged(Vector3 value);
public Quaternion Orientation
{
get => _orientation;
set { SetProperty(ref _orientation, value); OnOrientationChanged(value); }
}
partial void OnOrientationChanged(Quaternion value);
public float RotationAngleInDegrees
{
get => _rotationAngleInDegrees;
set { SetProperty(ref _rotationAngleInDegrees, value); OnRotationAngleInDegreesChanged(value); }
get => (float)MathEx.ToDegree(_rotationAngle);
set => RotationAngle = (float)MathEx.ToRadians(value);
}
public float RotationAngle
{
get => _rotationAngle;
set { SetProperty(ref _rotationAngle, value); OnRotationAngleChanged(value); }
}
partial void OnRotationAngleInDegreesChanged(float value);
partial void OnRotationAngleChanged(float value);
public Vector2 Size
{
......
......@@ -70,7 +70,7 @@ namespace Windows.UI.Composition
Update();
}
partial void OnRotationAngleInDegreesChanged(float value)
partial void OnRotationAngleChanged(float value)
{
UpdateTransform();
}
......
#nullable enable
//#define TRACE_COMPOSITION
using System;
using System.Linq;
using System.Numerics;
using SkiaSharp;
using Uno.Extensions;
using Uno.UI.Composition;
using Uno.UI.Composition.Composition;
namespace Windows.UI.Composition
namespace Windows.UI.Composition;
public partial class Visual : global::Windows.UI.Composition.CompositionObject
{
public partial class Visual : global::Windows.UI.Composition.CompositionObject
private CompositionClip? _clip;
private Vector2 _anchorPoint = Vector2.Zero; // Backing for scroll offsets
private int _zIndex;
public CompositionClip? Clip
{
get => _clip;
set => SetProperty(ref _clip, value);
}
public Vector2 AnchorPoint
{
get => _anchorPoint;
set
{
SetProperty(ref _anchorPoint, value);
Compositor.InvalidateRender();
}
}
internal int ZIndex
{
internal virtual void Render(SKSurface surface)
get => _zIndex;
set
{
if (_zIndex != value)
{
SetProperty(ref _zIndex, value);
if (Parent is ContainerVisual containerVisual)
{
containerVisual.IsChildrenRenderOrderDirty = true;
}
}
}
}
internal ShadowState? ShadowState { get; set; }
/// <summary>
/// Render a root visual.
/// </summary>
/// <param name="surface">The surface on which this visual should be rendered.</param>
/// <param name="ignoreLocation">A boolean which indicates if the location of the root visual should be ignored (so it will be rendered at 0,0).</param>
internal void RenderRootVisual(SKSurface surface, bool ignoreLocation = false)
{
if (this is { Opacity: 0 } or { IsVisible: false })
{
return;
}
public CompositionClip? Clip
if (ignoreLocation)
{
get;
set;
surface.Canvas.Save();
surface.Canvas.Translate(-(Offset.X + AnchorPoint.X), -(Offset.Y + AnchorPoint.Y));
}
// Backing for scroll offsets
private Vector2 _anchorPoint = Vector2.Zero;
private int _zIndex;
using var session = BeginDrawing(surface, DrawingFilters.Default);
Render(in session);
public Vector2 AnchorPoint
if (ignoreLocation)
{
get => _anchorPoint;
set
{
_anchorPoint = value;
Compositor.InvalidateRender();
}
surface.Canvas.Restore();
}
}
/// <summary>
/// Position a sub visual on the canvas and draw its content.
/// </summary>
/// <param name="parentSession">The drawing session of the <see cref="Parent"/> visual.</param>
internal virtual void Render(in DrawingSession parentSession)
{
#if TRACE_COMPOSITION
var indent = int.TryParse(Comment?.Split(new char[] { '-' }, 2, StringSplitOptions.TrimEntries).FirstOrDefault(), out var depth)
? new string(' ', depth * 2)
: string.Empty;
global::System.Diagnostics.Debug.WriteLine($"{indent}{Comment} (Opacity:{parentSession.Filters.Opacity:F2}x{Opacity:F2} | IsVisible:{IsVisible})");
#endif
internal int ZIndex
if (this is { Opacity: 0 } or { IsVisible: false })
{
get => _zIndex;
set
{
if (_zIndex != value)
{
_zIndex = value;
if (Parent is ContainerVisual containerVisual)
{
containerVisual.IsChildrenRenderOrderDirty = true;
}
}
}
return;
}
internal ShadowState? ShadowState { get; set; }
using var session = BeginDrawing(in parentSession);
Draw(in session);
}
/// <summary>
/// Draws the content of this visual.
/// </summary>
/// <param name="session">The drawing session to use.</param>
private protected virtual void Draw(in DrawingSession session)
{
}
private protected DrawingSession BeginDrawing(in DrawingSession parentSession)
=> BeginDrawing(parentSession.Surface, parentSession.Filters);
private protected DrawingSession BeginDrawing(SKSurface surface, in DrawingFilters filters)
{
if (ShadowState is { } shadow)
{
surface.Canvas.SaveLayer(shadow.Paint);
}
else
{
surface.Canvas.Save();
}
// Set the position of the visual on the canvas (i.e. change coordinates system to the "XAML element" one)
surface.Canvas.Translate(Offset.X + AnchorPoint.X, Offset.Y + AnchorPoint.Y);
// Applied rending transformation matrix (i.e. change coordinates system to the "rendering" one)
if (this.GetTransform() is { IsIdentity: false } transform)
{
var skTransform = transform.ToSKMatrix();
surface.Canvas.Concat(ref skTransform);
}
// Apply the clipping defined on the element
// (Only the Clip property, clipping applied by parent for layout constraints reason it's managed by the ShapeVisual through the ViewBox)
// Note: The Clip is applied after the transformation matrix, so it's also transformed.
Clip?.Apply(surface);
var session = new DrawingSession(surface, in filters);
DrawingSession.PushOpacity(ref session, Opacity);
return session;
}
}
......@@ -6,16 +6,11 @@ namespace Windows.UI.Composition
{
public partial class VisualCollection : CompositionObject, IEnumerable<Visual>
{
private readonly Visual _owner;
private readonly ContainerVisual _owner;
private List<Visual> _visuals = new List<Visual>();
private readonly List<Visual> _visuals = new();
internal VisualCollection(Compositor compositor, Visual owner) : base(compositor)
{
_owner = owner;
}
internal VisualCollection(Visual owner)
internal VisualCollection(Compositor compositor, ContainerVisual owner) : base(compositor)
{
_owner = owner;
}
......@@ -30,7 +25,7 @@ namespace Windows.UI.Composition
{
var index = _visuals.IndexOf(sibling);
_visuals.Insert(index, newChild);
newChild.Parent = _owner;
InsertAbovePartial(newChild, sibling);
CollectionChanged?.Invoke(this, EventArgs.Empty);
......@@ -41,6 +36,7 @@ namespace Windows.UI.Composition
public void InsertAtBottom(Visual newChild)
{
_visuals.Insert(0, newChild);
newChild.Parent = _owner;
InsertAtBottomPartial(newChild);
CollectionChanged?.Invoke(this, EventArgs.Empty);
......@@ -50,6 +46,7 @@ namespace Windows.UI.Composition
public void InsertAtTop(Visual newChild)
{
_visuals.Insert(_visuals.Count, newChild);
newChild.Parent = _owner;
InsertAtTopPartial(newChild);
CollectionChanged?.Invoke(this, EventArgs.Empty);
......@@ -60,6 +57,7 @@ namespace Windows.UI.Composition
{
var index = _visuals.IndexOf(sibling);
_visuals.Insert(index - 1, newChild);
newChild.Parent = _owner;
InsertBelowPartial(newChild, sibling);
CollectionChanged?.Invoke(this, EventArgs.Empty);
......@@ -68,15 +66,22 @@ namespace Windows.UI.Composition
public void Remove(Visual child)
{
_visuals.Remove(child);
RemovePartial(child);
if (_visuals.Remove(child))
{
child.Parent = null;
RemovePartial(child);
CollectionChanged?.Invoke(this, EventArgs.Empty);
CollectionChanged?.Invoke(this, EventArgs.Empty);
}
}
partial void RemovePartial(Visual child);
public void RemoveAll()
{
foreach (var child in _visuals)
{
child.Parent = null;
}
_visuals.Clear();
RemoveAllPartial();
......
......@@ -340,13 +340,7 @@ namespace Windows.UI.Composition
// Skipping already declared method Windows.UI.Composition.Compositor.CreateShapeVisual()
// Skipping already declared method Windows.UI.Composition.Compositor.CreateSpriteShape()
// Skipping already declared method Windows.UI.Composition.Compositor.CreateSpriteShape(Windows.UI.Composition.CompositionGeometry)
#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")]
public global::Windows.UI.Composition.CompositionViewBox CreateViewBox()
{
throw new global::System.NotImplementedException("The member CompositionViewBox Compositor.CreateViewBox() is not implemented. For more information, visit https://aka.platform.uno/notimplemented#m=CompositionViewBox%20Compositor.CreateViewBox%28%29");
}
#endif
// Skipping already declared method Windows.UI.Composition.Compositor.CreateViewBox()
#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")]
public global::Windows.Foundation.IAsyncAction RequestCommitAsync()
......
......@@ -13,34 +13,8 @@ namespace Windows.UI.Composition
// Skipping already declared property Scale
// Skipping already declared property RotationAxis
// Skipping already declared property RotationAngleInDegrees
#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")]
public float RotationAngle
{
get
{
throw new global::System.NotImplementedException("The member float Visual.RotationAngle is not implemented. For more information, visit https://aka.platform.uno/notimplemented#m=float%20Visual.RotationAngle");
}
set
{
global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.UI.Composition.Visual", "float Visual.RotationAngle");
}
}
#endif
#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")]
public global::System.Numerics.Quaternion Orientation
{
get
{
throw new global::System.NotImplementedException("The member Quaternion Visual.Orientation is not implemented. For more information, visit https://aka.platform.uno/notimplemented#m=Quaternion%20Visual.Orientation");
}
set
{
global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.UI.Composition.Visual", "Quaternion Visual.Orientation");
}
}
#endif
// Skipping already declared property RotationAngle
// Skipping already declared property Orientation
// Skipping already declared property Opacity
// Skipping already declared property Offset
// Skipping already declared property IsVisible
......
......@@ -209,7 +209,7 @@ internal partial class OpenGLWpfRenderer : IWpfRenderer
if (_host.RootElement?.Visual is { } rootVisual)
{
WinUI.Window.Current.Compositor.RenderRootVisual(_surface, rootVisual);
rootVisual.Compositor.RenderRootVisual(_surface, rootVisual);
}
}
......
......@@ -85,7 +85,7 @@ internal class SoftwareWpfRenderer : IWpfRenderer
surface.Canvas.SetMatrix(SKMatrix.CreateScale((float)dpiScaleX, (float)dpiScaleY));
if (_host.RootElement?.Visual is { } rootVisual)
{
WinUI.Window.Current.Compositor.RenderRootVisual(surface, rootVisual);
rootVisual.Compositor.RenderRootVisual(surface, rootVisual);
}
}
......
......@@ -50,19 +50,24 @@ public static partial class ImageAssert
public static void HasColorInRectangle(RawBitmap screenshot, Rectangle rect, Color expectedColor, byte tolerance = 0, [CallerLineNumber] int line = 0)
{
var bitmap = screenshot;
(int x, int y, int diff, Color color) min = (-1, -1, int.MaxValue, default);
for (var x = rect.Left; x < rect.Right; x++)
{
for (var y = rect.Top; y < rect.Bottom; y++)
{
var pixel = bitmap.GetPixel(x, y);
if (AreSameColor(expectedColor, pixel, tolerance, out _))
if (AreSameColor(expectedColor, pixel, tolerance, out var diff))
{
return;
}
else if (diff < min.diff)
{
min = (x, y, diff, pixel);
}
}
}
Assert.Fail($"Expected '{ToArgbCode(expectedColor)}' in rectangle '{rect}'.");
Assert.Fail($"Expected '{ToArgbCode(expectedColor)}' in rectangle '{rect}', but no pixel has this color. The closest pixel found is '{ToArgbCode(min.color)}' at '{min.x},{min.y}' with a (exclusive) difference of {min.diff}.");
}
/// <summary>
......@@ -76,9 +81,9 @@ public static partial class ImageAssert
for (var y = rect.Top; y < rect.Bottom; y++)
{
var pixel = bitmap.GetPixel(x, y);
if (AreSameColor(excludedColor, pixel, tolerance, out _))
if (AreSameColor(excludedColor, pixel, tolerance, out var diff))
{
Assert.Fail($"Color '{ToArgbCode(excludedColor)}' was found at ({x}, {y}) in rectangle '{rect}'.");
Assert.Fail($"Color '{ToArgbCode(excludedColor)}' was found at ({x}, {y}) in rectangle '{rect}' (Exclusive difference of {diff}).");
}
}
}
......@@ -209,8 +214,6 @@ public static partial class ImageAssert
using var reader1 = DataReader.FromBuffer(buffer1);
using var reader2 = DataReader.FromBuffer(buffer2);
var reader1Window = new byte[1024];
var reader2Window = new byte[1024];
while (reader1.UnconsumedBufferLength > 0 && reader2.UnconsumedBufferLength > 0)
{
if (reader1.ReadByte() != reader2.ReadByte())
......
......@@ -7,13 +7,16 @@ using System.Text;
using Windows.UI.Xaml.Media.Imaging;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Graphics.Display;
using Windows.Storage.Streams;
using Windows.UI.Xaml.Controls;
using Rectangle = System.Drawing.Rectangle;
using Size = System.Drawing.Size;
using Point = System.Drawing.Point;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using Uno.Helpers;
namespace Uno.UI.RuntimeTests.Helpers
{
......@@ -23,32 +26,48 @@ namespace Uno.UI.RuntimeTests.Helpers
public class RawBitmap
{
private byte[]? _pixels;
private readonly double _implicitScaling;
private bool _altered;
private RawBitmap(RenderTargetBitmap bitmap, UIElement renderedElement)
private RawBitmap(RenderTargetBitmap bitmap, UIElement renderedElement, double implicitScaling)
{
Bitmap = bitmap;
RenderedElement = renderedElement;
_implicitScaling = implicitScaling;
}
public static async Task<RawBitmap> From(RenderTargetBitmap bitmap, UIElement renderedElement)
/// <summary>
/// Prefer using UITestHelper.Screenshot() instead.
/// </summary>
public static async Task<RawBitmap> From(RenderTargetBitmap bitmap, UIElement renderedElement, double? implicitScaling = null)
{
var raw = new RawBitmap(bitmap, renderedElement);
implicitScaling ??= DisplayInformation.GetForCurrentView()?.RawPixelsPerViewPixel ?? 1;
var raw = new RawBitmap(bitmap, renderedElement, implicitScaling.Value);
await raw.Populate();
return raw;
}
public Size Size => new(Bitmap.PixelWidth, Bitmap.PixelHeight);
public double ImplicitScaling => _implicitScaling;
public Size Size => new(Width, Height);
public int Width => Bitmap.PixelWidth;
public int Height => Bitmap.PixelHeight;
public int Width => (int)(Bitmap.PixelWidth / _implicitScaling);
public int Height => (int)(Bitmap.PixelHeight / _implicitScaling);
/// <summary>
/// The rendered UIElement
/// </summary>
public UIElement RenderedElement { get; }
public RenderTargetBitmap Bitmap { get; }
/// <summary>
/// Gets the underlying <see cref="RenderTargetBitmap"/>.
/// Be aware this might not be the same as of the current state of the RawBitmap (cf. <see cref="MakeOpaque"/>).
/// Prefer to use the <see cref="GetImageSource"/>.
/// </summary>
internal RenderTargetBitmap Bitmap { get; }
public Color this[int x, int y] => GetPixel(x, y);
public Color GetPixel(int x, int y)
{
......@@ -57,7 +76,10 @@ namespace Uno.UI.RuntimeTests.Helpers
throw new InvalidOperationException("Populate must be invoked first");
}
var offset = (y * Width + x) * 4;
x = (int)(x * _implicitScaling);
y = (int)(y * _implicitScaling);
var offset = (y * Bitmap.PixelWidth + x) * 4;
var a = _pixels[offset + 3];
var r = _pixels[offset + 2];
var g = _pixels[offset + 1];
......@@ -66,14 +88,80 @@ namespace Uno.UI.RuntimeTests.Helpers
return Color.FromArgb(a, r, g, b);
}
public Color this[int x, int y] =>
GetPixel(x, y);
internal byte[] GetPixels()
{
if (_pixels is null)
{
throw new InvalidOperationException("Populate must be invoked first");
}
return _pixels;
}
/// <summary>
/// Enables the <see cref="GetPixel(int, int)"/> method.
/// </summary>
/// <returns></returns>
internal async Task Populate()
=> _pixels ??= (await Bitmap.GetPixelsAsync()).ToArray();
{
_pixels ??= (await Bitmap.GetPixelsAsync()).ToArray();
// Image is RGBA-premul, we need to un-multiply it to get the actual color in GetPixel().
ImageHelper.UnMultiplyAlpha(_pixels);
}
internal void MakeOpaque(Color? background = null)
{
if (_pixels is null)
{
throw new InvalidOperationException("Populate must be invoked first");
}
_altered = ImageHelper.MakeOpaque(_pixels, background);
}
internal async Task<ImageSource> GetImageSource(bool preferOriginal = false)
{
if (_pixels is null)
{
throw new InvalidOperationException("Populate must be invoked first");
}
if (_altered && !preferOriginal)
{
var output = new WriteableBitmap(Bitmap.PixelWidth, Bitmap.PixelHeight);
await new MemoryStream(_pixels).AsInputStream().ReadAsync(output.PixelBuffer, output.PixelBuffer.Length, InputStreamOptions.None);
return output;
}
else
{
return Bitmap;
}
}
#if __SKIA__ && DEBUG // DEBUG: Make the build to fail on CI to avoid forgetting to remove the call (would poluate server or other devs disks!).
/// <summary>
/// Save the screenshot into the specified path **for debug purposes only**.
/// </summary>
/// <param name="path"></param>
/// <param name="preferOriginal"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
internal async Task Save(string path, bool preferOriginal = false)
{
if (_pixels is null)
{
throw new InvalidOperationException("Populate must be invoked first");
}
await using var file = File.OpenWrite(path);
var img = preferOriginal
? SkiaSharp.SKImage.FromPixelCopy(new SkiaSharp.SKImageInfo(Bitmap.PixelWidth, Bitmap.PixelHeight, SkiaSharp.SKColorType.Bgra8888, SkiaSharp.SKAlphaType.Premul), (await Bitmap.GetPixelsAsync()).ToArray())
: SkiaSharp.SKImage.FromPixelCopy(new SkiaSharp.SKImageInfo(Bitmap.PixelWidth, Bitmap.PixelHeight, SkiaSharp.SKColorType.Bgra8888, SkiaSharp.SKAlphaType.Unpremul), _pixels);
img.Encode(SkiaSharp.SKEncodedImageFormat.Png, 100).SaveTo(file);
}
#endif
}
}
......@@ -2,19 +2,27 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Graphics.Display;
using Windows.Storage.Streams;
using Windows.System;
using Windows.UI.Input.Preview.Injection;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Markup;
using Private.Infrastructure;
using Uno.UI.RuntimeTests.Helpers;
using Windows.UI.Xaml.Media.Imaging;
using SamplesApp.UITests;
namespace Uno.UI.RuntimeTests.Tests.Uno_UI_Xaml_Core;
namespace Uno.UI.RuntimeTests.Helpers;
// Note: This file contains a bunch of helpers that are expected to be moved to the test engine among the pointer injection work
......@@ -30,16 +38,180 @@ public static class UITestHelper
return element.GetAbsoluteBounds();
}
public static async Task<RawBitmap> ScreenShot(FrameworkElement element, bool opaque = true)
/// <summary>
/// Takes a screen-shot of the given element.
/// </summary>
/// <param name="element">The element to screen-shot.</param>
/// <param name="opaque">Indicates if the resulting image should be make opaque (i.e. all pixels has an opacity of 0xFF) or not.</param>
/// <param name="scaling">Indicates the scaling strategy to apply for the image (when screen is not using a 1.0 scale, usually 4K screens).</param>
/// <returns></returns>
public static async Task<RawBitmap> ScreenShot(FrameworkElement element, bool opaque = false, ScreenShotScalingMode scaling = ScreenShotScalingMode.UsePhysicalPixelsWithImplicitScaling)
{
var renderer = new RenderTargetBitmap();
element.UpdateLayout();
await TestServices.WindowHelper.WaitForIdle();
await renderer.RenderAsync(element);
var bitmap = await RawBitmap.From(renderer, element);
RawBitmap bitmap;
switch (scaling)
{
case ScreenShotScalingMode.UsePhysicalPixelsWithImplicitScaling:
await renderer.RenderAsync(element);
bitmap = await RawBitmap.From(renderer, element, DisplayInformation.GetForCurrentView()?.RawPixelsPerViewPixel ?? 1);
break;
case ScreenShotScalingMode.UseLogicalPixels:
await renderer.RenderAsync(element, (int)element.RenderSize.Width, (int)element.RenderSize.Height);
bitmap = await RawBitmap.From(renderer, element);
break;
case ScreenShotScalingMode.UsePhysicalPixels:
await renderer.RenderAsync(element);
bitmap = await RawBitmap.From(renderer, element);
break;
default:
throw new NotSupportedException($"Mode {scaling} is not supported.");
}
if (opaque)
{
bitmap.MakeOpaque();
}
return bitmap;
}
public enum ScreenShotScalingMode
{
/// <summary>
/// Screen-shot is made at full resolution, then the returned RawBitmap is configured to implicitly apply screen scaling
/// to requested pixel coordinates in <see cref="RawBitmap.GetPixel"/> method.
///
/// This is best / common option has it avoids artifacts due image scaling while still allowing to use logical pixels.
/// </summary>
UsePhysicalPixelsWithImplicitScaling,
/// <summary>
/// Screen-shot is made at full resolution, and access to the returned <see cref="RawBitmap"/> are assumed to be in physical pixels.
/// </summary>
UsePhysicalPixels,
/// <summary>
/// Screen-shot is forcefully scaled down to logical pixels.
/// </summary>
UseLogicalPixels
}
/// <summary>
/// Shows the given screenshot on screen for debug purposes
/// </summary>
/// <param name="bitmap">The image to show.</param>
/// <returns></returns>
public static async Task Show(RawBitmap bitmap)
{
Image img;
CompositeTransform imgTr;
TextBlock pos;
var popup = new ContentDialog
{
MinWidth = bitmap.Width + 2,
MinHeight = bitmap.Height + 30,
Content = new Grid
{
RowDefinitions =
{
new RowDefinition(),
new RowDefinition { Height = GridLength.Auto }
},
Children =
{
new Border
{
BorderBrush = new SolidColorBrush(Windows.UI.Colors.Black),
BorderThickness = new Thickness(1),
Background = new SolidColorBrush(Windows.UI.Colors.Gray),
Width = bitmap.Width * bitmap.ImplicitScaling + 2,
Height = bitmap.Height * bitmap.ImplicitScaling + 2,
Child = img = new Image
{
Width = bitmap.Width * bitmap.ImplicitScaling,
Height = bitmap.Height * bitmap.ImplicitScaling,
Source = await bitmap.GetImageSource(),
Stretch = Stretch.None,
ManipulationMode = ManipulationModes.Scale
| ManipulationModes.ScaleInertia
| ManipulationModes.TranslateX
| ManipulationModes.TranslateY
| ManipulationModes.TranslateInertia,
RenderTransformOrigin = new Point(.5, .5),
RenderTransform = imgTr = new CompositeTransform()
}
},
new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Children =
{
(pos = new TextBlock
{
Text = $"{bitmap.Width}x{bitmap.Height}",
FontSize = 8
})
}
}.Apply(e => Grid.SetRow(e, 1))
}
},
PrimaryButtonText = "OK"
};
img.PointerMoved += (snd, e) => DumpState(e.GetCurrentPoint(img).Position);
img.PointerWheelChanged += (snd, e) =>
{
if (e.KeyModifiers is VirtualKeyModifiers.Control
&& e.GetCurrentPoint(img) is { Properties.IsHorizontalMouseWheel: false } point)
{
var factor = Math.Sign(point.Properties.MouseWheelDelta) is 1 ? 1.2 : 1 / 1.2;
imgTr.ScaleX *= factor;
imgTr.ScaleY *= factor;
DumpState(point.Position);
}
};
img.ManipulationDelta += (snd, e) =>
{
imgTr.TranslateX += e.Delta.Translation.X;
imgTr.TranslateY += e.Delta.Translation.Y;
imgTr.ScaleX *= e.Delta.Scale;
imgTr.ScaleY *= e.Delta.Scale;
DumpState(e.Position);
};
void DumpState(Point phyLoc)
{
var scaling = bitmap.ImplicitScaling;
var virLoc = new Point(phyLoc.X / scaling, phyLoc.Y / scaling);
var virSize = bitmap.Size;
var phySize = new Size(virSize.Width * scaling, virSize.Height * scaling);
if (virLoc.X >= 0 && virLoc.X < virSize.Width
&& virLoc.Y >= 0 && virLoc.Y < virSize.Height)
{
if (scaling is not 1.0)
{
pos.Text = $"{imgTr.ScaleX:P0} {bitmap.GetPixel((int)virLoc.X, (int)virLoc.Y)} | vir: {virLoc.X:F0},{virLoc.Y:F0} / {virSize.Width}x{virSize.Height} | phy: {phyLoc.X:F0},{phyLoc.Y:F0} / {phySize.Width}x{phySize.Height}";
}
else
{
pos.Text = $"{imgTr.ScaleX:P0} {bitmap.GetPixel((int)virLoc.X, (int)virLoc.Y)} | {phyLoc.X:F0},{phyLoc.Y:F0} / {virSize.Width}x{virSize.Height}";
}
}
else
{
pos.Text = $"{imgTr.ScaleX:P0} {bitmap.Width}x{bitmap.Height}";
}
}
await popup.ShowAsync(ContentDialogPlacement.Popup);
}
}
public class DynamicDataTemplate : IDisposable
......
......@@ -124,12 +124,7 @@ namespace Uno.UI.RuntimeTests.Tests.Microsoft_UI_Xaml_Controls
}
}
private async Task<RawBitmap> TakeScreenshot(FrameworkElement SUT)
{
var renderer = new RenderTargetBitmap();
await renderer.RenderAsync(SUT);
var result = await RawBitmap.From(renderer, SUT);
return result;
}
private Task<RawBitmap> TakeScreenshot(FrameworkElement SUT)
=> UITestHelper.ScreenShot(SUT);
}
}
......@@ -15,6 +15,7 @@ using Windows.UI.Xaml.Media;
using FluentAssertions;
using Private.Infrastructure;
using Uno.Extensions;
using Uno.UI.RuntimeTests.Helpers;
namespace Uno.UI.RuntimeTests.Tests.Uno_UI_Xaml_Core;
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Shapes;
using Uno.Extensions;
using Uno.UI.RuntimeTests.Helpers;
namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Composition;
[TestClass]
internal class Given_CompositionClip
{
#if __SKIA__
[TestMethod]
[RunsOnUIThread]
public async Task When_TransformElementClippedByParent_Then_ClippingAppliedPostRendering()
{
var sut = new Grid
{
Width = 300,
Height = 300,
Background = new SolidColorBrush(Colors.Chartreuse),
Children =
{
new Grid
{
Width = 200,
Height = 200,
Background = new SolidColorBrush(Colors.DeepPink),
Children =
{
new Grid
{
Width = 300,
Height = 300,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Background = new SolidColorBrush(Colors.DeepSkyBlue),
RenderTransform = new TranslateTransform { Y = 200 },
}
}
}
}
};
var expected = new Canvas
{
Width = 300,
Height = 300,
Children =
{
new Rectangle
{
Width = 300,
Height = 300,
Fill = new SolidColorBrush(Colors.Chartreuse),
},
new Rectangle
{
Width = 200,
Height = 200,
Fill = new SolidColorBrush(Colors.DeepPink),
}
.Apply(r => Canvas.SetLeft(r, 50))
.Apply(r => Canvas.SetTop(r, 50)),
new Rectangle
{
Width = 200,
Height = 50,
Fill = new SolidColorBrush(Colors.DeepSkyBlue),
}
.Apply(r => Canvas.SetLeft(r, 50))
.Apply(r => Canvas.SetTop(r, 200)),
}
};
var (actual, expectedImg) = await Render(sut, expected);
// First we check some well known important points
ImageAssert.HasColorAt(actual, 150, 1, Colors.Chartreuse);
ImageAssert.HasColorAt(actual, 150, 49, Colors.Chartreuse);
ImageAssert.HasColorAt(actual, 150, 151, Colors.DeepPink);
ImageAssert.HasColorAt(actual, 150, 199, Colors.DeepPink);
ImageAssert.HasColorAt(actual, 150, 201, Colors.DeepSkyBlue);
ImageAssert.HasColorAt(actual, 150, 249, Colors.DeepSkyBlue);
ImageAssert.HasColorAt(actual, 150, 299, Colors.Chartreuse);
// Second we do a full comparison of something that should be identical but drawn using another (simpler ?) mechanism
await ImageAssert.AreEqualAsync(actual, expectedImg);
}
[TestMethod]
[RunsOnUIThread]
public async Task When_TransformElementClippedByParentWithBorder_Then_ClippingAppliedPostRendering()
{
var sut = new Grid
{
Width = 300,
Height = 300,
Name = "Chartreuse",
BorderBrush = new SolidColorBrush(Colors.Chartreuse),
BorderThickness = new Thickness(5),
Background = new SolidColorBrush(Colors.Chartreuse.WithOpacity(.5)),
Children =
{
new Grid
{
Width = 200,
Height = 200,
Name = "Pink",
BorderBrush = new SolidColorBrush(Colors.DeepPink),
BorderThickness = new Thickness(5),
Background = new SolidColorBrush(Colors.DeepPink.WithOpacity(.5)),
Children =
{
new Grid
{
Width = 300,
Height = 300,
Name = "Blue",
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
BorderBrush = new SolidColorBrush(Colors.DeepSkyBlue),
BorderThickness = new Thickness(5),
Background = new SolidColorBrush(Colors.DeepSkyBlue.WithOpacity(.5)),
RenderTransform = new TranslateTransform { Y = 200 },
}
}
}
}
};
var expected = new Canvas
{
Width = 300,
Height = 300,
Children =
{
new Rectangle
{
Width = 300,
Height = 300,
Fill = new SolidColorBrush(Colors.Chartreuse.WithOpacity(.5)),
Stroke = new SolidColorBrush(Colors.Chartreuse),
StrokeThickness = 5,
},
new Rectangle
{
Width = 200,
Height = 200,
Fill = new SolidColorBrush(Colors.DeepPink.WithOpacity(.5)),
Stroke = new SolidColorBrush(Colors.DeepPink),
StrokeThickness = 5,
}
.Apply(r => Canvas.SetLeft(r, 50))
.Apply(r => Canvas.SetTop(r, 50)),
new Rectangle
{
Width = 190,
Height = 45,
Fill = new SolidColorBrush(Colors.DeepSkyBlue.WithOpacity(.5)),
}
.Apply(r => Canvas.SetLeft(r, 55))
.Apply(r => Canvas.SetTop(r, 200)),
new Line
{
X1 = 55,
Y1 = 200,
X2 = 55 + 190,
Y2 = 200,
Stroke = new SolidColorBrush(Colors.DeepSkyBlue),
StrokeThickness = 5,
}
}
};
var (actual, expectedImg) = await Render(sut, expected);
// First we check some well known important points
ImageAssert.HasColorAt(actual, 1, 201, Colors.Chartreuse);
//ImageAssert.HasColorAt(actual, 51, 201, Colors.DeepPink); // Known limitation, the blue is overflowing over the pink border
ImageAssert.HasColorAt(actual, 56, 201, Colors.DeepSkyBlue);
ImageAssert.HasColorAt(actual, 243, 201, Colors.DeepSkyBlue);
ImageAssert.HasColorAt(actual, 246, 201, Colors.DeepPink);
ImageAssert.HasColorAt(actual, 299, 201, Colors.Chartreuse);
// Second we do a full comparison of something that should be identical but drawn using another (simpler ?) mechanism
//await ImageAssert.AreEqualAsync(actual, expectedImg); // Known limitation, the blue is overflowing over the pink border
}
[TestMethod]
[RunsOnUIThread]
public async Task When_MixTransformCliPropertyAndClippedByParentWithBorders_Then_RenderingIsValid()
{
var sut = new Grid
{
Width = 230,
Height = 120,
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Left,
Children =
{
new Grid
{
Name = "red",
Width = 100,
Height = 100,
Background = new SolidColorBrush(Colors.Red.WithOpacity(.5)),
BorderBrush = new SolidColorBrush(Colors.Red),
BorderThickness = new Thickness(6),
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Left,
RenderTransform = new CompositeTransform { ScaleX = 2 },
Clip = new RectangleGeometry { Rect = new Rect(0, 0, 110, 110) },
Children =
{
new Rectangle
{
Name = "blue",
Width = 150,
Height = 100,
Fill = new SolidColorBrush(Colors.Blue.WithOpacity(.5)),
Stroke = new SolidColorBrush(Colors.Blue),
StrokeThickness = 5,
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Left,
RenderTransform = new TranslateTransform { X = 50, Y = 50 }
},
new Border
{
Name = "green",
Width = 150,
Height = 100,
BorderBrush = new SolidColorBrush(Colors.Green),
BorderThickness = new Thickness(6),
Background = new SolidColorBrush(Colors.Green.WithOpacity(.5)),
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Left,
RenderTransform = new TranslateTransform { X = 50, Y = -50 },
Child = new Rectangle
{
Name = "orange",
Width = 50,
Height = 50,
Fill = new SolidColorBrush(Colors.Orange),
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Left,
RenderTransform = new TranslateTransform { X = 25, Y = 0 }
}
}
}
}
}
};
var expected = new Canvas
{
Width = 230,
Height = 120,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Children =
{
new Rectangle { Width = 200, Height = 100, Fill = new SolidColorBrush(Colors.Red.WithOpacity(.5)) },
new Line { X1 = 6, X2 = 6, Y1 = 0, Y2 = 100, Stroke = new SolidColorBrush(Colors.Red), StrokeThickness = 12 },
new Line { X1 = 0, X2 = 200, Y1 = 3, Y2 = 3, Stroke = new SolidColorBrush(Colors.Red), StrokeThickness = 6 },
new Line { X1 = 194, X2 = 194, Y1 = 0, Y2 = 100, Stroke = new SolidColorBrush(Colors.Red), StrokeThickness = 12 },
new Line { X1 = 200, X2 = 0, Y1 = 97, Y2 = 97, Stroke = new SolidColorBrush(Colors.Red), StrokeThickness = 6 },
new Rectangle { Width = 108, Height = 44, Fill = new SolidColorBrush(Colors.Green.WithOpacity(.5)) }.Apply(e => Canvas.SetLeft(e, 112)),
new Line { X1 = 118, X2 = 118, Y1 = 0, Y2 = 44, Stroke = new SolidColorBrush(Colors.Green), StrokeThickness = 12 },
new Rectangle { Width = 46, Height = 12, Fill = new SolidColorBrush(Colors.Orange) }.Apply(e => Canvas.SetLeft(e, 174)),
new Rectangle { Width = 108, Height = 54, Fill = new SolidColorBrush(Colors.Blue.WithOpacity(.5)) }.Apply(e => Canvas.SetLeft(e, 112)).Apply(e => Canvas.SetTop(e, 56)),
new Line { X1 = 118, X2 = 118, Y1 = 56, Y2 = 110, Stroke = new SolidColorBrush(Colors.Blue), StrokeThickness = 12 },
new Line { X1 = 112, X2 = 220, Y1 = 59, Y2 = 59, Stroke = new SolidColorBrush(Colors.Blue), StrokeThickness = 6 }
}
};
var (actual, expectedImg) = await Render(sut, expected);
// First we check some well known important points
ImageAssert.HasColorAt(actual, 106, 1, Colors.Red);
ImageAssert.HasColorAt(actual, 118, 1, Colors.Green);
ImageAssert.DoesNotHaveColorAt(actual, 172, 1, Colors.Orange);
ImageAssert.HasColorAt(actual, 176, 1, Colors.Orange);
ImageAssert.HasColorAt(actual, 118, 42, Colors.Green);
ImageAssert.DoesNotHaveColorAt(actual, 118, 46, Colors.Green);
ImageAssert.DoesNotHaveColorAt(actual, 118, 54, Colors.Blue);
ImageAssert.HasColorAt(actual, 118, 58, Colors.Blue);
ImageAssert.HasColorAt(actual, 118, 100, Colors.Blue);
ImageAssert.HasColorAt(actual, 118, 108, Colors.Blue);
ImageAssert.DoesNotHaveColorAt(actual, 118, 112, Colors.Blue); // clipped by Clip property
ImageAssert.HasColorAt(actual, 200, 58, Colors.Blue);
ImageAssert.HasColorAt(actual, 218, 58, Colors.Blue);
ImageAssert.DoesNotHaveColorAt(actual, 222, 58, Colors.Blue); // clipped by Clip property
// Second we do a full comparison of something that should be identical but drawn using another (simpler ?) mechanism
// Note: We give a pixel tolerance because of the antialiasing and scaling
var expectedPixels = ExpectedPixels
.At(0, 0)
.Pixels(expectedImg)
.WithPixelTolerance(2, 2, LocationToleranceKind.PerPixel)
.WithColorTolerance(32);
ImageAssert.HasPixels(actual, expectedPixels);
}
private async Task<(RawBitmap actual, RawBitmap expected)> Render(FrameworkElement sut, FrameworkElement expected)
{
#if DEBUG
await UITestHelper.Load(new Grid
{
ColumnDefinitions =
{
new ColumnDefinition(),
new ColumnDefinition()
},
Children =
{
sut.Apply(e => Grid.SetColumn(e, 0)),
expected.Apply(e => Grid.SetColumn(e, 1))
}
});
return (await UITestHelper.ScreenShot(sut), await UITestHelper.ScreenShot(expected));
#else
await UITestHelper.Load(expected);
var expectedImg = await UITestHelper.ScreenShot(expected);
// We render the sut in second so the final screenshot is the one of the sut
await UITestHelper.Load(sut);
var actual = await UITestHelper.ScreenShot(sut);
return (actual, expectedImg);
#endif
}
#endif
}
......@@ -36,17 +36,9 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
{
var SUT = new FrameworkElement_Opacity();
TestServices.WindowHelper.WindowContent = SUT;
await UITestHelper.Load(SUT);
await TestServices.WindowHelper.WaitForIdle();
await TestServices.WindowHelper.WaitForIdle();
var renderer = new RenderTargetBitmap();
await renderer.RenderAsync(SUT);
var si = await RawBitmap.From(renderer, SUT);
var si = await UITestHelper.ScreenShot(SUT);
var width = SUT.tbOpacity1_0.ActualWidth;
var height = SUT.tbOpacity1_0.ActualHeight;
......@@ -74,15 +66,10 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
{
var SUT = new FrameworkElement_Opacity();
TestServices.WindowHelper.WindowContent = SUT;
await TestServices.WindowHelper.WaitForIdle();
var renderer = new RenderTargetBitmap();
await UITestHelper.Load(SUT);
await renderer.RenderAsync(SUT);
var si = await UITestHelper.ScreenShot(SUT);
var si = await RawBitmap.From(renderer, SUT);
var width = SUT.tbInnerOpacity1_0.ActualWidth;
var height = SUT.tbInnerOpacity1_0.ActualHeight;
ImageAssert.HasColorAtChild(si, SUT.tbInnerOpacity1_0, (width / 4) * 3.3, height / 2, "#FF808080");
......
......@@ -660,10 +660,10 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml
var clip = new Rect(0, 0, 50, 50);
sut.ArrangeVisual(rect, clip);
Assert.IsNotNull(sut.Visual.Clip);
Assert.IsNotNull(sut.Visual.ViewBox);
sut.ArrangeVisual(rect, null);
Assert.IsNull(sut.Visual.Clip);
Assert.IsNull(sut.Visual.ViewBox);
}
#endif
}
......
......@@ -200,6 +200,7 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml
Assert.IsNotNull(SUT.tb01);
Assert.IsNotNull(SUT.tb02);
// Note: If not null, this usually means that the control is leaking!!!
await AssertIsNullAsync(() => SUT.panel01);
await AssertIsNullAsync(() => SUT.tb03);
await AssertIsNullAsync(() => SUT.panel02);
......
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
......@@ -15,11 +14,11 @@ using Windows.UI.Xaml.Shapes;
using Windows.Foundation.Metadata;
using Uno.UI;
using static Private.Infrastructure.TestServices;
using Rectangle = System.Drawing.Rectangle;
using Windows.Media.Core;
using Uno.UI.RuntimeTests.Extensions;
using Windows.UI.Composition;
using System.IO;
using Windows.Foundation;
using Windows.UI.Input.Preview.Injection;
using Uno.Extensions;
using Uno.UI.RuntimeTests.Tests.Uno_UI_Xaml_Core;
......@@ -325,6 +324,72 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
);
}
[TestMethod]
#if __ANDROID__ || __IOS__ || __WASM__
[Ignore("Not supported yet")]
#endif
public async Task Border_CornerRadiusAndClip_Clipping()
{
var sut = new Border
{
Name = "sut",
Height = 150,
Width = 150,
CornerRadius = new CornerRadius(50),
BorderBrush = new SolidColorBrush(Colors.Red),
BorderThickness = new Thickness(20),
Background = new SolidColorBrush(Colors.Blue),
Clip = new RectangleGeometry
{
Rect = new Rect(10, 10, 130, 130)
},
Child = new Rectangle
{
Name = "the_nested_rectangle",
Fill = new SolidColorBrush(Colors.Green),
Width = 150,
Height = 150
}
};
var root = new Border { Child = sut };
await UITestHelper.Load(root);
var snapshot = await UITestHelper.ScreenShot(root);
ImageAssert.HasPixels(
snapshot,
ExpectedPixels
.At($"top-left-content-radius", 30, 30)
.WithColorTolerance(1)
.Pixel(Colors.Red),
ExpectedPixels
.At($"top-right-content-radius", 30, 120)
.WithColorTolerance(1)
.Pixel(Colors.Red),
ExpectedPixels
.At($"bottom-left-content-radius", 120, 30)
.WithColorTolerance(1)
.Pixel(Colors.Red),
ExpectedPixels
.At($"bottom-right-content-radius", 120, 120)
.WithColorTolerance(1)
.Pixel(Colors.Red),
ExpectedPixels
.At($"left-clip", 5, 75)
.Pixel(Colors.Transparent),
ExpectedPixels
.At($"top-clip", 75, 5)
.Pixel(Colors.Transparent),
ExpectedPixels
.At($"right-clip", 145, 75)
.Pixel(Colors.Transparent),
ExpectedPixels
.At($"bottom-clip", 145, 145)
.Pixel(Colors.Transparent)
);
}
[TestMethod]
public async Task Border_LinearGradient()
{
......@@ -421,7 +486,7 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
var firstBorderRect = SUT.GetRelativeCoords(SUT.FirstBorder);
var secondBorderRect = SUT.GetRelativeCoords(SUT.SecondBorder);
var rect = new Rectangle((int)firstBorderRect.X, (int)firstBorderRect.Y,
var rect = new System.Drawing.Rectangle((int)firstBorderRect.X, (int)firstBorderRect.Y,
(int)firstBorderRect.Width + 1, (int)firstBorderRect.Height + 1);
await WindowHelper.WaitForIdle();
......@@ -468,14 +533,8 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
private async Task<RawBitmap> TakeScreenshot(FrameworkElement SUT)
{
WindowHelper.WindowContent = SUT;
await WindowHelper.WaitForLoaded(SUT);
var renderer = new RenderTargetBitmap();
await WindowHelper.WaitForIdle();
await renderer.RenderAsync(SUT);
var result = await RawBitmap.From(renderer, SUT);
await WindowHelper.WaitForIdle();
return result;
await UITestHelper.Load(SUT);
return await UITestHelper.ScreenShot(SUT);
}
}
}
......@@ -18,7 +18,6 @@ using Windows.UI.Xaml;
using MUXControlsTestApp;
using Windows.Foundation.Metadata;
namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
{
[TestClass]
......@@ -59,13 +58,8 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
}
var canvas = new Canvas_With_Outer_Clip();
WindowHelper.WindowContent = canvas;
await WindowHelper.WaitForLoaded(canvas);
var renderer = new RenderTargetBitmap();
await WindowHelper.WaitForIdle();
await renderer.RenderAsync(canvas);
var bitmap = await RawBitmap.From(renderer, canvas);
await UITestHelper.Load(canvas);
var bitmap = await UITestHelper.ScreenShot(canvas);
var clippedLocation = canvas.Get_LocatorBorder1();
await WindowHelper.WaitForIdle();
......@@ -95,13 +89,8 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
}
var canvas = new CanvasZIndex();
WindowHelper.WindowContent = canvas;
await WindowHelper.WaitForLoaded(canvas);
var renderer = new RenderTargetBitmap();
await WindowHelper.WaitForIdle();
await renderer.RenderAsync(canvas);
var bitmap = await RawBitmap.From(renderer, canvas);
await UITestHelper.Load(canvas);
var bitmap = await UITestHelper.ScreenShot(canvas);
var redBorderRect1 = canvas.Get_CanvasBorderRed1();
ImageAssert.HasColorAtChild(bitmap, redBorderRect1, (float)redBorderRect1.Width / 2, (float)redBorderRect1.Height / 2, Green);
......@@ -138,13 +127,8 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
}
var canvas = new Canvas_In_Canvas();
WindowHelper.WindowContent = canvas;
await WindowHelper.WaitForLoaded(canvas);
var renderer = new RenderTargetBitmap();
await WindowHelper.WaitForIdle();
await renderer.RenderAsync(canvas);
var bitmap = await RawBitmap.From(renderer, canvas);
await UITestHelper.Load(canvas);
var bitmap = await UITestHelper.ScreenShot(canvas);
var clippedLocation = canvas.Get_CanvasBorderBlue1();
ImageAssert.HasColorAtChild(bitmap, clippedLocation, (float)clippedLocation.Width / 2, clippedLocation.Height / 2, Blue);
......
......@@ -607,14 +607,7 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
ImageAssert.HasColorAt(screenshot, screenshot.Width / 2, screenshot.Height - 5, Color.FromArgb(0xFF, 0xED, 0x1B, 0x24), tolerance: 5);
}
private async Task<RawBitmap> TakeScreenshot(FrameworkElement SUT)
{
var renderer = new RenderTargetBitmap();
await WindowHelper.WaitForIdle();
await renderer.RenderAsync(SUT);
var result = await RawBitmap.From(renderer, SUT);
await WindowHelper.WaitForIdle();
return result;
}
private Task<RawBitmap> TakeScreenshot(FrameworkElement SUT)
=> UITestHelper.ScreenShot(SUT);
}
}
......@@ -16,7 +16,7 @@ using Windows.UI.ViewManagement;
using static Private.Infrastructure.TestServices;
using Uno.Disposables;
using Uno.Extensions;
using Uno.UI.RuntimeTests.Tests.Uno_UI_Xaml_Core;
using Uno.UI.RuntimeTests.Helpers;
using Uno.UI.Toolkit.Extensions;
namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
......
......@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Uno.UI.RuntimeTests.Helpers;
using Uno.Helpers;
using Windows.Foundation;
using Windows.Foundation.Metadata;
using Windows.UI;
......@@ -254,13 +255,10 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
Foreground = new SolidColorBrush(Colors.Red) { Opacity = 0.5 },
};
WindowHelper.WindowContent = SUT;
await WindowHelper.WaitForIdle();
await UITestHelper.Load(SUT);
var bitmap = await UITestHelper.ScreenShot(SUT);
var renderer = new RenderTargetBitmap();
await renderer.RenderAsync(SUT);
var bitmap = await RawBitmap.From(renderer, SUT);
ImageAssert.HasColorInRectangle(bitmap, new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), Color.FromArgb(127, 127, 0, 0));
ImageAssert.HasColorInRectangle(bitmap, new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), Colors.Red.WithOpacity(.5));
}
}
}
......@@ -11,7 +11,7 @@ using FluentAssertions;
using Microsoft.UI.Xaml.Controls;
#endif
using Private.Infrastructure;
using Uno.UI.RuntimeTests.Tests.Uno_UI_Xaml_Core;
using Uno.UI.RuntimeTests.Helpers;
namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls.Repeater;
......
......@@ -197,12 +197,8 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media
bindableView.RemoveView(view);
}
#endif
WindowHelper.WindowContent = SUT;
await WindowHelper.WaitForLoaded(SUT);
var renderer = new RenderTargetBitmap();
await WindowHelper.WaitForIdle();
await renderer.RenderAsync(SUT);
var result = await RawBitmap.From(renderer, SUT);
await UITestHelper.Load(SUT);
var result = await UITestHelper.ScreenShot(SUT);
await WindowHelper.WaitForIdle();
return result;
}
......
......@@ -60,7 +60,6 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media
WindowHelper.WindowContent = SUT;
await WindowHelper.WaitForLoaded(SUT);
var renderer = new RenderTargetBitmap();
const float BorderOffset =
#if __SKIA__
7;
......@@ -85,8 +84,7 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media
_ => throw new ArgumentOutOfRangeException($"unexpected stretch: {stretch}"),
};
await renderer.RenderAsync(SUT);
var bitmap = await RawBitmap.From(renderer, SUT);
var bitmap = await UITestHelper.ScreenShot(SUT);
ImageAssert.HasColorAt(bitmap, centerX, BorderOffset, expectations.Top, tolerance: 5);
ImageAssert.HasColorAt(bitmap, centerX, height - BorderOffset, expectations.Bottom, tolerance: 5);
......
......@@ -16,7 +16,6 @@ using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Media;
using FluentAssertions;
using Uno.Extensions;
using Uno.UI.RuntimeTests.Tests.Uno_UI_Xaml_Core;
using static Private.Infrastructure.TestServices;
namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media
......
......@@ -180,10 +180,7 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media_Imaging
await WindowHelper.WaitForIdle();
var renderer = new RenderTargetBitmap();
await WindowHelper.WaitForIdle();
await renderer.RenderAsync(parent);
var snapshot = await RawBitmap.From(renderer, rect);
var snapshot = await UITestHelper.ScreenShot(parent);
var coords = parent.GetRelativeCoords(rect);
await WindowHelper.WaitForIdle();
......
......@@ -9,6 +9,10 @@ using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Imaging;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;
using Windows.Graphics.Display;
using Windows.UI;
using Windows.UI.Xaml.Media;
using FluentAssertions;
using Uno.UI.RuntimeTests.Helpers;
namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media_Imaging
......@@ -17,14 +21,9 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media_Imaging
[RunsOnUIThread]
public class Given_RenderTargetBitmap
{
private static readonly Windows.UI.Xaml.Media.SolidColorBrush Background
= new Windows.UI.Xaml.Media.SolidColorBrush(Windows.UI.Color.FromArgb(255, 0, 0, 255));
private static readonly Windows.UI.Xaml.Media.SolidColorBrush BorderBrush
= new Windows.UI.Xaml.Media.SolidColorBrush(Windows.UI.Color.FromArgb(125, 125, 0, 0));
private static readonly System.Numerics.Vector2 BorderSize
= new(10, 10);
static readonly char[] map = { 'B', 'G', 'R', 'A' };
private static readonly Windows.UI.Xaml.Media.SolidColorBrush Background = new(Windows.UI.Color.FromArgb(255, 0, 0, 255));
private static readonly Windows.UI.Xaml.Media.SolidColorBrush BorderBrush = new(Windows.UI.Color.FromArgb(125, 125, 0, 0));
private static readonly System.Numerics.Vector2 BorderSize = new(10, 10);
[TestMethod]
#if __WASM__
......@@ -38,6 +37,12 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media_Imaging
#endif
public async Task When_Render_Border_GetPixelsAsync()
{
if (DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel is not 1.0)
{
Assert.Inconclusive("This test is not compatible with non-default display scaling.");
return;
}
var border = new Border()
{
Name = "TestBorder",
......@@ -48,38 +53,36 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media_Imaging
BorderBrush = BorderBrush,
};
var resurcename = GetType().Assembly
var resourceName = GetType()
.Assembly
.GetManifestResourceNames()
.FirstOrDefault(name => name.EndsWith("Border_Snapshot.bgra8"));
Assert.IsNotNull(resurcename, "Do not find resorce named Border_Snapshot.bgra8");
Assert.IsNotNull(resourceName, "Do not find resource named Border_Snapshot.bgra8");
var rawBorderSnapshot = GetType().Assembly.GetManifestResourceStream(resurcename)
var rawBorderSnapshot = GetType()
.Assembly
.GetManifestResourceStream(resourceName)
.ReadAllBytes();
TestServices.WindowHelper.WindowContent = border;
await TestServices.WindowHelper.WaitForLoaded(border);
//Ensure Layouted
await TestServices.WindowHelper.WaitForIdle();
await UITestHelper.Load(border);
Assert.AreEqual(BorderSize, border.ActualSize, "Invalid Layouted.");
var renderer = new RenderTargetBitmap();
await renderer.RenderAsync(border);
var pixels = await renderer.GetPixelsAsync();
var pixels = (await renderer.GetPixelsAsync()).ToArray();
//Using of the loop instead of IsSameData method to get more information in case of failure.
var pixelsArray = pixels.ToArray();
Assert.AreEqual(rawBorderSnapshot.Length, pixelsArray.Length, $"Invalid length. Expected {rawBorderSnapshot.Length} found {pixelsArray.Length}.");
Assert.AreEqual(rawBorderSnapshot.Length, pixels.Length, $"Invalid length. Expected {rawBorderSnapshot.Length} found {pixels.Length}.");
for (int i = 0; i < pixelsArray.Length; i++)
for (var i = 0; i < pixels.Length; i += 4)
{
var pixel = i / 4;
var component = i % 4;
Assert.AreEqual(rawBorderSnapshot[i], pixelsArray[i], $"The {map[component]} channel of pixel {pixel} is not same. Expected {rawBorderSnapshot[i]:x2} found {pixelsArray[i]:x2}.");
Assert.AreEqual(rawBorderSnapshot[i + 3], pixels[i + 3], $"The A channel of pixel {i / 4} is not same. Expected {rawBorderSnapshot[i + 3]:x2} found {pixels[i + 3]:x2}.");
Assert.AreEqual(rawBorderSnapshot[i + 2], pixels[i + 2], $"The R channel of pixel {i / 4} is not same. Expected {rawBorderSnapshot[i + 2]:x2} found {pixels[i + 2]:x2}.");
Assert.AreEqual(rawBorderSnapshot[i + 1], pixels[i + 1], $"The G channel of pixel {i / 4} is not same. Expected {rawBorderSnapshot[i + 1]:x2} found {pixels[i + 1]:x2}.");
Assert.AreEqual(rawBorderSnapshot[i], pixels[i], $"The B channel of pixel {i / 4} is not same. Expected {rawBorderSnapshot[i]:x2} found {pixels[i]:x2}.");
}
}
......@@ -101,10 +104,7 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media_Imaging
BorderBrush = BorderBrush
};
TestServices.WindowHelper.WindowContent = border;
await TestServices.WindowHelper.WaitForLoaded(border);
await TestServices.WindowHelper.WaitForIdle();
await UITestHelper.Load(border);
var sut = new RenderTargetBitmap();
await sut.RenderAsync(border);
......@@ -128,11 +128,36 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media_Imaging
await TestServices.WindowHelper.WaitForIdle();
// We are also using RenderTargetBitmap to validate the result ... weird but it works :)
var resultTarget = new RenderTargetBitmap();
await resultTarget.RenderAsync(onCanvas);
var result = await RawBitmap.From(resultTarget, onCanvas);
var result = await UITestHelper.ScreenShot(onCanvas);
ImageAssert.HasColorAt(result, 5, 5, Background.Color);
}
[TestMethod]
#if __WASM__
[Ignore("Not implemented yet.")]
#elif __MACOS__
[Ignore("Currently fails on macOS, part of #9282 epic")]
#endif
public async Task When_RenderNotOpaqueContent_Then_ImageNotOpaque()
{
var nonOpaqueColor = Color.FromArgb(0x80, 0xCC, 0x00, 0x00);
var border = new Border()
{
Name = "TestBorder",
Width = 10,
Height = 10,
Background = new SolidColorBrush(nonOpaqueColor),
};
await UITestHelper.Load(border);
// Note: We do not use the UITestHelper.ScreenShot to ensure usage of the RenderTargetBitmap!
var sut = new RenderTargetBitmap();
await sut.RenderAsync(border);
var result = await RawBitmap.From(sut, border);
ImageAssert.HasColorAt(result, 5, 5, nonOpaqueColor, tolerance: 1);
}
}
}
#nullable enable
using System;
using System.Linq;
using Windows.UI;
namespace Uno.Helpers;
internal static class ColorExtensions
{
/// <summary>
/// Returns the color that results from blending the color with the given background color.
/// </summary>
/// <param name="color">The color to blend.</param>
/// <param name="background">The background color to use. This is assumed to be opaque (not checked for perf reason when used on pixel buffer).</param>
/// <returns>The color that results from blending the color with the given background color.</returns>
internal static Color ToOpaque(this Color color, Color background)
=> Color.FromArgb(
255,
(byte)(((byte.MaxValue - color.A) * background.R + color.A * color.R) / 255),
(byte)(((byte.MaxValue - color.A) * background.G + color.A * color.G) / 255),
(byte)(((byte.MaxValue - color.A) * background.B + color.A * color.B) / 255)
);
#if !HAS_UNO
internal static Color WithOpacity(this Color color, double opacity)
=> Color.FromArgb((byte)(color.A * opacity), color.R, color.G, color.B);
#endif
}
#nullable enable
using System;
using System.Linq;
using Windows.UI;
namespace Uno.Helpers;
internal static class ImageHelper
{
/// <summary>
/// Make all pixels of the given buffer opaque.
/// </summary>
/// <param name="rgba8PixelsBuffer">The pixels buffer (must not be pre-multiplied!).</param>
/// <param name="background">The **opaque** background to use.</param>
public static bool MakeOpaque(byte[] rgba8PixelsBuffer, Color? background = null)
{
if (background is { A: not 255 })
{
throw new ArgumentException("The background color must be opaque.", nameof(background));
}
background ??= new Color { A = 255, R = 255, G = 255, B = 255 }; // White
var modified = false;
for (var i = 0; i < rgba8PixelsBuffer.Length; i += 4)
{
var a = rgba8PixelsBuffer[i + 3];
if (a == 255)
{
continue;
}
var r = rgba8PixelsBuffer[i + 2];
var g = rgba8PixelsBuffer[i + 1];
var b = rgba8PixelsBuffer[i + 0];
var opaque = Color.FromArgb(a, r, g, b).ToOpaque(background.Value);
rgba8PixelsBuffer[i + 3] = opaque.A; // 255
rgba8PixelsBuffer[i + 2] = opaque.R;
rgba8PixelsBuffer[i + 1] = opaque.G;
rgba8PixelsBuffer[i + 0] = opaque.B;
modified = true;
}
return modified;
}
/// <summary>
/// Un-multiply the alpha channel of each pixel of the given buffer.
/// </summary>
/// <param name="rgba8PremulPixelsBuffer">The pixel buffer.</param>
public static void UnMultiplyAlpha(byte[] rgba8PremulPixelsBuffer)
{
for (var i = 0; i < rgba8PremulPixelsBuffer.Length; i += 4)
{
var a = rgba8PremulPixelsBuffer[i + 3];
var r = rgba8PremulPixelsBuffer[i + 2];
var g = rgba8PremulPixelsBuffer[i + 1];
var b = rgba8PremulPixelsBuffer[i + 0];
//a = a;
r = (byte)(255.0 * r / a);
g = (byte)(255.0 * g / a);
b = (byte)(255.0 * b / a);
rgba8PremulPixelsBuffer[i + 2] = r;
rgba8PremulPixelsBuffer[i + 1] = g;
rgba8PremulPixelsBuffer[i + 0] = b;
}
}
}
......@@ -95,13 +95,13 @@ namespace Uno.UI.Extensions
private static readonly IndentationFormat _defaultIndentationFormat = IndentationFormat.NumberedSpaces;
/// <summary>
/// The name (<see cref="GetDebugName"/>) indented (<see cref="GetDebugIndent(object?)"/>) for log usage.
/// The name (<see cref="GetDebugName"/>) indented (<see cref="GetDebugIndent(object?, IndentationFormat, bool)"/>) for log usage.
/// </summary>
internal static string GetDebugIdentifier(this object? elt)
=> GetDebugIdentifier(elt, _defaultIndentationFormat);
/// <summary>
/// The name (<see cref="GetDebugName"/>) indented (<see cref="GetDebugIndent(object?)"/>) for log usage.
/// The name (<see cref="GetDebugName"/>) indented (<see cref="GetDebugIndent(object?, IndentationFormat, bool)"/>) for log usage.
/// </summary>
internal static string GetDebugIdentifier(this object? elt, IndentationFormat format)
=> $"{elt.GetDebugIndent(format)} [{elt.GetDebugName()}]";
......
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
using Windows.Foundation;
namespace System.Numerics
{
public static class VectorExtensions
{
/// <summary>
/// Converts a <see cref="Vector2"/> to <see cref="Point"/>
/// </summary>
/// <param name="vector">The <see cref="Vector2"/> to convert</param>
/// <returns>A <see cref="Point"/></returns>
public static Point ToPoint(this Vector2 vector) => new Point(vector.X, vector.Y);
/// <summary>
/// Converts a <see cref="Vector2"/> to <see cref="Size"/>
/// </summary>
/// <param name="vector">The <see cref="Vector2"/> to convert</param>
/// <returns>A <see cref="Size"/></returns>
public static Size ToSize(this Vector2 vector) => new Size(vector.X, vector.Y);
/// <summary>
/// Converts a <see cref="Point"/> to <see cref="Vector2"/>
/// </summary>
/// <param name="point">The <see cref="Point"/> to Convert</param>
/// <returns>A <see cref="Vector2"/></returns>
public static Vector2 ToVector2(this Point point) => new Vector2((float)point.X, (float)point.Y);
/// <summary>
/// Converts a <see cref="Size"/> to <see cref="Vector2"/>
/// </summary>
/// <param name="point">The <see cref="Size"/> to Convert</param>
/// <returns>A <see cref="Vector2"/></returns>
public static Vector2 ToVector2(this Size size) => new Vector2((float)size.Width, (float)size.Height);
}
}
......@@ -7,6 +7,7 @@ using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Media;
using Uno.Extensions;
namespace Microsoft.UI.Xaml.Controls;
......
......@@ -84,11 +84,13 @@ public partial class RefreshContainer : ContentControl
m_refreshVisualizerPresenter = (Panel)GetTemplateChild("RefreshVisualizerPresenter");
// END: Populate template children
#if !HAS_UNO
if (m_root != null)
{
var rootVisual = ElementCompositionPreview.GetElementVisual(m_root);
rootVisual.Clip = rootVisual.Compositor.CreateInsetClip(0.0f, 0.0f, 0.0f, 0.0f);
}
#endif
m_refreshVisualizer = Visualizer;
if (m_refreshVisualizer == null)
......
......@@ -10,6 +10,7 @@ using Windows.UI.Core;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
using Uno.Extensions;
using Uno.UI;
using Uno.UI.Xaml.Core;
using Windows.UI.Xaml;
......
......@@ -94,7 +94,7 @@ namespace Windows.UI.Xaml.Controls
UpdateBorder();
}
bool ICustomClippingElement.AllowClippingToLayoutSlot => !(Child is UIElement ue) || ue.RenderTransform == null;
bool ICustomClippingElement.AllowClippingToLayoutSlot => true;
bool ICustomClippingElement.ForceClippingToLayoutSlot => CornerRadius != CornerRadius.None;
}
}
......@@ -90,20 +90,16 @@ namespace Windows.UI.Xaml.Shapes
return Disposable.Empty;
}
var parent = owner.Visual;
var compositor = parent.Compositor;
var visual = owner.Visual;
var compositor = visual.Compositor;
var background = state.Background;
var borderThickness = owner.LayoutRound(state.BorderThickness);
var borderBrush = state.BorderBrush;
var cornerRadius = state.CornerRadius;
var disposables = new CompositeDisposable();
var sublayers = new List<Visual>();
var shapes = new List<CompositionShape>();
var heightOffset = ((float)borderThickness.Top / 2) + ((float)borderThickness.Bottom / 2);
var widthOffset = ((float)borderThickness.Left / 2) + ((float)borderThickness.Right / 2);
var halfWidth = (float)area.Width / 2;
var halfHeight = (float)area.Height / 2;
var adjustedArea = state.BackgroundSizing == BackgroundSizing.InnerBorderEdge
? area.DeflateBy(borderThickness)
: area;
......@@ -112,81 +108,82 @@ namespace Windows.UI.Xaml.Shapes
if (!fullCornerRadius.IsEmpty)
{
var borderShape = compositor.CreateSpriteShape();
var backgroundShape = compositor.CreateSpriteShape();
var outerShape = compositor.CreateSpriteShape();
var clipShape = compositor.CreateSpriteShape();
// Border brush
Brush.AssignAndObserveBrush(borderBrush, compositor, brush => borderShape.FillBrush = brush)
.DisposeWith(disposables);
// Background brush
if (background is ImageBrush imgBackground)
{
adjustedArea = CreateImageLayer(compositor, disposables, borderThickness, adjustedArea, backgroundShape, adjustedArea, imgBackground);
}
else
{
Brush.AssignAndObserveBrush(background, compositor, brush => backgroundShape.FillBrush = brush)
.DisposeWith(disposables);
}
// This needs to be adjusted if multiple UI threads are used in the future for multi-window
fullCornerRadius.Outer.GetRadii(_outerRadiiStore);
fullCornerRadius.Inner.GetRadii(_innerRadiiStore);
// Background shape
var backgroundPath = state.BackgroundSizing == BackgroundSizing.InnerBorderEdge ?
GetRoundedPath(adjustedArea.ToSKRect(), _innerRadiiStore) :
GetRoundedPath(area.ToSKRect(), _outerRadiiStore);
backgroundShape.Geometry = compositor.CreatePathGeometry(backgroundPath);
var backgroundVisual = compositor.CreateShapeVisual();
backgroundVisual.Shapes.Add(backgroundShape);
sublayers.Add(backgroundVisual);
parent.Children.InsertAtBottom(backgroundVisual);
// Background shape (if any)
if (background is not null)
{
var backgroundShape = compositor.CreateSpriteShape();
// First we set the brush as it might alter the adjustedArea
if (background is ImageBrush imgBackground)
{
adjustedArea = CreateImageLayer(compositor, disposables, borderThickness, adjustedArea, backgroundShape, adjustedArea, imgBackground);
}
else
{
Brush.AssignAndObserveBrush(background, compositor, brush => backgroundShape.FillBrush = brush)
.DisposeWith(disposables);
}
// Then we create the geometry
var backgroundPath = state.BackgroundSizing == BackgroundSizing.InnerBorderEdge
? GetBackgroundPath(_innerRadiiStore, adjustedArea)
: GetBackgroundPath(_outerRadiiStore, area);
backgroundShape.Geometry = compositor.CreatePathGeometry(backgroundPath);
#if DEBUG
backgroundShape.Comment = "#background";
#endif
// Finally we add the shape to the visual (so it will invalidate the rendering only once)
visual.Shapes.Add(backgroundShape);
shapes.Add(backgroundShape);
}
// Border shape (if any)
if (borderThickness != Thickness.Empty)
{
var borderPath = GetBorderPath(_outerRadiiStore, _innerRadiiStore, area, adjustedArea);
borderShape.Geometry = compositor.CreatePathGeometry(borderPath);
var borderShape = compositor.CreateSpriteShape();
var borderVisual = compositor.CreateShapeVisual();
// Border brush
Brush.AssignAndObserveBrush(borderBrush, compositor, brush => borderShape.FillBrush = brush)
.DisposeWith(disposables);
borderVisual.Shapes.Add(borderShape);
sublayers.Add(borderVisual);
// Then we create the geometry
var borderPath = GetBorderPath(_outerRadiiStore, _innerRadiiStore, area, adjustedArea);
borderShape.Geometry = compositor.CreatePathGeometry(borderPath);
#if DEBUG
borderShape.Comment = "#border";
#endif
parent.Children.InsertAtTop(borderVisual);
// Finally we add the shape to the visual (so it will invalidate the rendering only once)
visual.Shapes.Add(borderShape);
shapes.Add(borderShape);
}
// Note: The clipping is used to determine the location where the children of current element can be rendered.
// So its has to be the "inner" area (i.e. the area without the border).
// The border and the background shapes are already clipped properly and will be drawn without that clipping property set.
owner.ClippingIsSetByCornerRadius = true;
parent.Clip = compositor.CreateRectangleClip(
0, 0, (float)area.Width, (float)area.Height,
fullCornerRadius.Outer.TopLeft, fullCornerRadius.Outer.TopRight, fullCornerRadius.Outer.BottomRight, fullCornerRadius.Outer.BottomLeft);
visual.Clip = compositor.CreateRectangleClip(
(float)adjustedArea.Left, (float)adjustedArea.Top, (float)adjustedArea.Right, (float)adjustedArea.Bottom,
fullCornerRadius.Inner.TopLeft, fullCornerRadius.Inner.TopRight, fullCornerRadius.Inner.BottomRight, fullCornerRadius.Inner.BottomLeft);
}
else
{
var shapeVisual = compositor.CreateShapeVisual();
var backgroundShape = compositor.CreateSpriteShape();
var backgroundArea = area;
// Background brush
if (background is ImageBrush imgBackground)
{
backgroundArea = CreateImageLayer(compositor, disposables, borderThickness, adjustedArea, backgroundShape, backgroundArea, imgBackground);
CreateImageLayer(compositor, disposables, borderThickness, adjustedArea, backgroundShape, area, imgBackground);
}
else
{
Brush.AssignAndObserveBrush(background, compositor, brush => backgroundShape.FillBrush = brush)
.DisposeWith(disposables);
// This is required because changing the CornerRadius changes the background drawing
// implementation and we don't want a rectangular background behind a rounded background.
Disposable.Create(() => backgroundShape.FillBrush = null)
.DisposeWith(disposables);
}
var geometrySource = new SkiaGeometrySource2D();
......@@ -195,91 +192,93 @@ namespace Windows.UI.Xaml.Shapes
geometry.AddRect(adjustedArea.ToSKRect());
backgroundShape.Geometry = compositor.CreatePathGeometry(new CompositionPath(geometrySource));
#if DEBUG
backgroundShape.Comment = "#background";
#endif
shapeVisual.Shapes.Add(backgroundShape);
visual.Shapes.Add(backgroundShape);
shapes.Add(backgroundShape);
// Border shape (if any)
if (borderThickness != Thickness.Empty)
{
Action<Action<CompositionSpriteShape, SKPath>> createLayer = builder =>
void CreateLayer(Action<CompositionSpriteShape, SKPath> builder, string name)
{
var spriteShape = compositor.CreateSpriteShape();
var geometry = new SkiaGeometrySource2D();
// Border brush
Brush.AssignAndObserveBrush(borderBrush, compositor, brush => spriteShape.StrokeBrush = brush)
.DisposeWith(disposables);
.DisposeWith(disposables);
builder(spriteShape, geometry.Geometry);
spriteShape.Geometry = compositor.CreatePathGeometry(new CompositionPath(geometry));
#if DEBUG
spriteShape.Comment = name;
#endif
shapeVisual.Shapes.Add(spriteShape);
};
visual.Shapes.Add(spriteShape);
shapes.Add(spriteShape);
}
if (borderThickness.Top != 0)
{
createLayer((l, path) =>
CreateLayer((l, path) =>
{
l.StrokeThickness = (float)borderThickness.Top;
var StrokeThicknessAdjust = (float)(borderThickness.Top / 2);
path.MoveTo((float)(area.X + borderThickness.Left), (float)(area.Y + StrokeThicknessAdjust));
path.LineTo((float)(area.X + area.Width - borderThickness.Right), (float)(area.Y + StrokeThicknessAdjust));
path.Close();
});
}, "#border-top");
}
if (borderThickness.Bottom != 0)
{
createLayer((l, path) =>
CreateLayer((l, path) =>
{
l.StrokeThickness = (float)borderThickness.Bottom;
var StrokeThicknessAdjust = borderThickness.Bottom / 2;
path.MoveTo((float)(area.X + (float)borderThickness.Left), (float)(area.Y + area.Height - StrokeThicknessAdjust));
path.LineTo((float)(area.X + area.Width - (float)borderThickness.Right), (float)(area.Y + area.Height - StrokeThicknessAdjust));
path.Close();
});
}, "#border-bottom");
}
if (borderThickness.Left != 0)
{
createLayer((l, path) =>
CreateLayer((l, path) =>
{
l.StrokeThickness = (float)borderThickness.Left;
var StrokeThicknessAdjust = borderThickness.Left / 2;
path.MoveTo((float)(area.X + StrokeThicknessAdjust), (float)area.Y);
path.LineTo((float)(area.X + StrokeThicknessAdjust), (float)(area.Y + area.Height));
path.Close();
});
}, "#border-left");
}
if (borderThickness.Right != 0)
{
createLayer((l, path) =>
CreateLayer((l, path) =>
{
l.StrokeThickness = (float)borderThickness.Right;
var StrokeThicknessAdjust = borderThickness.Right / 2;
path.MoveTo((float)(area.X + area.Width - StrokeThicknessAdjust), (float)area.Y);
path.LineTo((float)(area.X + area.Width - StrokeThicknessAdjust), (float)(area.Y + area.Height));
path.Close();
});
}, "#border-right");
}
}
sublayers.Add(shapeVisual);
// Must be inserted below the other subviews, which may happen when
// the current view has subviews.
parent.Children.InsertAtBottom(shapeVisual);
}
disposables.Add(() =>
{
owner.ClippingIsSetByCornerRadius = false;
foreach (var sv in sublayers)
foreach (var shape in shapes)
{
parent.Children.Remove(sv);
sv.Dispose();
visual.Shapes.Remove(shape);
shape.Dispose();
}
}
);
......@@ -315,7 +314,7 @@ namespace Windows.UI.Xaml.Shapes
if (imgBackground.Transform != null)
{
matrix *= imgBackground.Transform.ToMatrix(new Point());
matrix *= imgBackground.Transform.MatrixCore;
}
surfaceBrush.TransformMatrix = matrix;
......@@ -334,36 +333,32 @@ namespace Windows.UI.Xaml.Shapes
return backgroundArea;
}
private static CompositionPath GetRoundedPath(SKRect area, SKPoint[] radii, SkiaGeometrySource2D geometrySource = null)
private static CompositionPath GetBackgroundPath(SKPoint[] radii, Rect area)
{
geometrySource ??= new SkiaGeometrySource2D();
var geometry = geometrySource.Geometry;
var roundRect = CreateRoundRect(area, radii);
geometry.AddRoundRect(roundRect);
geometry.Close();
var geometrySource = new SkiaGeometrySource2D();
SetRoundedPath(geometrySource.Geometry, area.ToSKRect(), radii);
return new CompositionPath(geometrySource);
}
private static SKRoundRect CreateRoundRect(SKRect area, SKPoint[] radii)
{
var roundRect = new SKRoundRect();
roundRect.SetRectRadii(area, radii);
return roundRect;
}
private static CompositionPath GetBorderPath(SKPoint[] outerRadii, SKPoint[] innerRadii, Rect area, Rect insetArea)
private static CompositionPath GetBorderPath(SKPoint[] outerRadii, SKPoint[] innerRadii, Rect outerArea, Rect innerArea)
{
var geometrySource = new SkiaGeometrySource2D();
GetRoundedPath(area.ToSKRect(), outerRadii, geometrySource);
GetRoundedPath(insetArea.ToSKRect(), innerRadii, geometrySource);
SetRoundedPath(geometrySource.Geometry, outerArea.ToSKRect(), outerRadii);
SetRoundedPath(geometrySource.Geometry, innerArea.ToSKRect(), innerRadii);
geometrySource.Geometry.FillType = SKPathFillType.EvenOdd;
return new CompositionPath(geometrySource);
}
private static void SetRoundedPath(in SKPath geometry, SKRect area, SKPoint[] radii)
{
var roundRect = new SKRoundRect();
roundRect.SetRectRadii(area, radii);
geometry.AddRoundRect(roundRect);
geometry.Close();
}
private class LayoutState : IEquatable<LayoutState>
{
public readonly Rect Area;
......
......@@ -31,6 +31,14 @@ namespace Windows.UI.Xaml.Controls
Visual.Children.InsertAtBottom(_textVisual);
}
#if DEBUG
private protected override void OnLoaded()
{
base.OnLoaded();
_textVisual.Comment = $"{Visual.Comment}#text";
}
#endif
protected override Size MeasureOverride(Size availableSize)
{
var padding = Padding;
......
......@@ -12,6 +12,7 @@ using Windows.Storage;
using Windows.UI.Text;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Uno.UI.Composition;
#nullable enable
......@@ -19,16 +20,19 @@ namespace Windows.UI.Composition
{
internal class TextVisual : Visual
{
private readonly TextBlock _owner;
private readonly WeakReference<TextBlock> _owner;
public TextVisual(Compositor compositor, TextBlock owner) : base(compositor)
{
_owner = owner;
_owner = new WeakReference<TextBlock>(owner);
}
internal override void Render(SKSurface surface)
private protected override void Draw(in DrawingSession session)
{
_owner.Inlines.Render(surface, Compositor);
if (_owner.TryGetTarget(out var owner))
{
owner.Inlines.Draw(in session);
}
}
}
}
......@@ -8,6 +8,7 @@ using Windows.UI.Composition;
using Windows.UI.Text;
using Windows.UI.Xaml.Documents.TextFormatting;
using Windows.UI.Xaml.Media;
using Uno.UI.Composition;
#nullable enable
......@@ -291,14 +292,14 @@ namespace Windows.UI.Xaml.Documents
/// <summary>
/// Renders a block-level inline collection, i.e. one that belongs to a TextBlock (or Paragraph, in the future).
/// </summary>
internal void Render(SKSurface surface, Compositor compositor)
internal void Draw(in DrawingSession session)
{
if (_renderLines.Count == 0)
{
return;
}
var canvas = surface.Canvas;
var canvas = session.Surface.Canvas;
var parent = (IBlock)_collection.GetParent();
var alignment = parent.TextAlignment;
......@@ -339,7 +340,7 @@ namespace Windows.UI.Xaml.Documents
red: scb.Color.R,
green: scb.Color.G,
blue: scb.Color.B,
alpha: (byte)(scb.Color.A * scb.Opacity * compositor.CurrentOpacity));
alpha: (byte)(scb.Color.A * scb.Opacity * session.Filters.Opacity));
}
var decorations = inline.TextDecorations;
......
......@@ -3,6 +3,7 @@ using System.Numerics;
using Uno.Disposables;
using Windows.UI.Composition;
using Windows.UI;
using Uno.Extensions;
using Microsoft.UI.Xaml.Media;
using System.Collections.Generic;
using Uno;
......
using System.Numerics;
using SkiaSharp;
using Windows.UI.Composition;
using Uno.Extensions;
namespace Windows.UI.Xaml.Media
{
......
......@@ -9,6 +9,7 @@ using Windows.Foundation;
using Java.Nio;
using Android.Views;
using Uno.UI.Xaml.Media;
using Uno.Extensions;
using System.Runtime.InteropServices;
namespace Windows.UI.Xaml.Media.Imaging
......
......@@ -152,7 +152,7 @@ namespace Windows.UI.Xaml.Media.Imaging
void IDisposable.Dispose()
{
if (_buffer is { })
if (_buffer is not null)
{
ArrayPool<byte>.Shared.Return(_buffer);
}
......
......@@ -4,9 +4,11 @@ using System;
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using Windows.Foundation;
using Windows.Graphics.Display;
using CoreGraphics;
using Foundation;
using UIKit;
using Uno.UI;
using Uno.UI.Xaml.Media;
namespace Windows.UI.Xaml.Media.Imaging
......@@ -45,8 +47,7 @@ namespace Windows.UI.Xaml.Media.Imaging
private static (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIElement element, ref byte[]? buffer, Size? scaledSize = null)
{
var size = new Size(element.ActualSize.X, element.ActualSize.Y);
if (size.IsEmpty)
if (size == default)
{
return (0, 0, 0);
}
......@@ -54,7 +55,8 @@ namespace Windows.UI.Xaml.Media.Imaging
UIImage uiImage;
try
{
UIGraphics.BeginImageContextWithOptions(size, false, 1f);
var scale = (float)(DisplayInformation.GetForCurrentView()?.RawPixelsPerViewPixel ?? 1.0);
UIGraphics.BeginImageContextWithOptions(size, false, scale);
var ctx = UIGraphics.GetCurrentContext();
ctx.SetFillColor(Colors.Transparent); // This is only for pixels not used, but the bitmap as the same size of the element. We keep it only for safety!
element.Layer.RenderInContext(ctx);
......
......@@ -2,6 +2,7 @@
using System;
using System.Runtime.InteropServices;
using Windows.Foundation;
using Windows.Graphics.Display;
using Windows.UI.Composition;
using Uno.UI.Xaml.Media;
using SkiaSharp;
......@@ -42,18 +43,21 @@ namespace Windows.UI.Xaml.Media.Imaging
var renderSize = element.RenderSize;
var visual = element.Visual;
if (element.RenderSize is { IsEmpty: true }
|| element.RenderSize is { Width: 0, Height: 0 })
if (renderSize is { IsEmpty: true } or { Width: 0, Height: 0 })
{
return (0, 0, 0);
}
var (width, height) = ((int)renderSize.Width, (int)renderSize.Height);
// Note: RenderTargetBitmap returns images with the current DPI (a 50x50 Border rendered on WinUI will return a 75x75 image)
var dpi = DisplayInformation.GetForCurrentView()?.RawPixelsPerViewPixel ?? 1;
var (width, height) = ((int)(renderSize.Width * dpi), (int)(renderSize.Height * dpi));
var info = new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
using var surface = SKSurface.Create(info);
//Ensure Clear
var canvas = surface.Canvas;
canvas.Clear(SKColors.Transparent);
visual.Render(surface);
canvas.Scale((float)dpi);
visual.RenderRootVisual(surface, ignoreLocation: true);
var img = surface.Snapshot();
......
using System.Numerics;
using SkiaSharp;
using Windows.UI.Composition;
using Uno.Extensions;
namespace Windows.UI.Xaml.Media
{
......
......@@ -43,7 +43,7 @@ namespace Windows.UI.Xaml.Media
};
/// <summary>
/// Notifies that a value of this transform changed (usually this means that the <see cref="Matrix"/> has been updated).
/// Notifies that a value of this transform changed (usually this means that the <see cref="MatrixCore"/> has been updated).
/// </summary>
internal event EventHandler Changed;
......@@ -66,7 +66,7 @@ namespace Windows.UI.Xaml.Media
/// <summary>
/// The matrix used by this transformation
/// </summary>
/// <remarks>This matrix does not include any center point</remarks>
/// <remarks>This matrix does not include any origin point (i.e. equivalent to `.ToMatrix(default(Point))`)</remarks>
internal Matrix3x2 MatrixCore { get; private set; } = Matrix3x2.Identity;
/// <summary>
......
......@@ -408,7 +408,7 @@ namespace Windows.UI.Xaml.Media
if (element is ScrollViewer sv)
TRACE($"- scroll viewer: zoom={sv.ZoomFactor:F2}");
if (element.RenderTransform is { } tr)
TRACE($"- renderTransform: {tr.ToMatrix(element.RenderTransformOrigin)}");
TRACE($"- renderTransform: {tr.ToMatrix(element.RenderTransformOrigin, element.ActualSize.ToSize())}");
// First compute the transformation between the element and its parent coordinate space
var matrix = Matrix3x2.Identity;
......
......@@ -8,8 +8,6 @@ namespace Windows.UI.Xaml.Shapes
{
partial class Ellipse : Shape
{
private ShapeVisual _rectangleVisual;
static Ellipse()
{
StretchProperty.OverrideMetadata(typeof(Ellipse), new FrameworkPropertyMetadata(defaultValue: Media.Stretch.Fill));
......@@ -17,8 +15,6 @@ namespace Windows.UI.Xaml.Shapes
public Ellipse()
{
_rectangleVisual = Visual.Compositor.CreateShapeVisual();
Visual.Children.InsertAtBottom(_rectangleVisual);
}
protected override Size MeasureOverride(Size availableSize)
......
using System;
using System.Collections.Generic;
using System.Text;
using Windows.UI.Xaml.Media;
using Uno.Extensions;
using System.Linq;
using Uno.Disposables;
using Uno.UI.Extensions;
using Uno.UI;
#nullable enable
using System;
using Windows.UI.Composition;
using Windows.Foundation;
using Windows.Graphics;
using System.Numerics;
namespace Windows.UI.Xaml.Shapes
......@@ -33,17 +26,9 @@ namespace Windows.UI.Xaml.Shapes
protected override Size ArrangeOverride(Size finalSize)
{
var (shapeSize, renderingArea) = ArrangeRelativeShape(finalSize);
SkiaGeometrySource2D path;
if (renderingArea.Width > 0 && renderingArea.Height > 0)
{
path = GetGeometry(renderingArea);
}
else
{
path = null;
}
var path = renderingArea.Width > 0 && renderingArea.Height > 0
? GetGeometry(renderingArea)
: null;
Render(path);
......@@ -52,33 +37,17 @@ namespace Windows.UI.Xaml.Shapes
private SkiaGeometrySource2D GetGeometry(Rect finalRect)
{
var strokeThickness = StrokeThickness;
var radiusX = RadiusX;
var radiusY = RadiusY;
var offset = new Vector2((float)(finalRect.Left), (float)(finalRect.Top));
var offset = new Vector2((float)finalRect.Left, (float)finalRect.Top);
var size = new Vector2((float)finalRect.Width, (float)finalRect.Height);
SkiaGeometrySource2D geometry;
if (radiusX == 0 || radiusY == 0)
{
// Simple rectangle
geometry = new SkiaGeometrySource2D(
CompositionGeometry.BuildRectangleGeometry(
offset,
size));
}
else
{
// Complex rectangle
geometry = new SkiaGeometrySource2D(
CompositionGeometry.BuildRoundedRectangleGeometry(
offset,
size,
new Vector2((float)radiusX, (float)radiusY)));
}
var geometry = radiusX is 0 || radiusY is 0
? CompositionGeometry.BuildRectangleGeometry(offset, size)
: CompositionGeometry.BuildRoundedRectangleGeometry(offset, size, new Vector2((float)radiusX, (float)radiusY));
return geometry;
return new SkiaGeometrySource2D(geometry);
}
}
}
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Specialized;
......@@ -17,20 +19,25 @@ using System.Numerics;
namespace Windows.UI.Xaml.Shapes
{
[Markup.ContentProperty(Name = "SvgChildren")]
partial class Shape
{
private ShapeVisual _rectangleVisual;
private SerialDisposable _fillSubscription = new SerialDisposable();
private SerialDisposable _strokeSubscription = new SerialDisposable();
private CompositionSpriteShape _pathSpriteShape;
private readonly SerialDisposable _fillSubscription = new();
private readonly SerialDisposable _strokeSubscription = new();
private readonly CompositionSpriteShape _shape;
private readonly CompositionPathGeometry _geometry;
public Shape()
{
_rectangleVisual = Visual.Compositor.CreateShapeVisual();
Visual.Children.InsertAtBottom(_rectangleVisual);
var visual = Visual;
var compositor = visual.Compositor;
_geometry = compositor.CreatePathGeometry();
_shape = compositor.CreateSpriteShape(_geometry);
#if DEBUG
_shape.Comment = "#path";
#endif
InitCommonShapeProperties();
visual.Shapes.Add(_shape);
}
private Rect GetPathBoundingBox(SkiaGeometrySource2D path)
......@@ -38,29 +45,19 @@ namespace Windows.UI.Xaml.Shapes
private bool IsFinite(double value) => !double.IsInfinity(value);
private void InitCommonShapeProperties() { }
private protected void Render(Windows.UI.Composition.SkiaGeometrySource2D path, double? scaleX = null, double? scaleY = null, double? renderOriginX = null, double? renderOriginY = null)
private protected void Render(Windows.UI.Composition.SkiaGeometrySource2D? path, double? scaleX = null, double? scaleY = null, double? renderOriginX = null, double? renderOriginY = null)
{
var compositionPath = new CompositionPath(path);
var pathGeometry = Visual.Compositor.CreatePathGeometry();
pathGeometry.Path = compositionPath;
_pathSpriteShape = Visual.Compositor.CreateSpriteShape(pathGeometry);
if (scaleX != null && scaleY != null)
{
_pathSpriteShape.Scale = new Vector2((float)scaleX.Value, (float)scaleY.Value);
}
else
if (path is null)
{
_pathSpriteShape.Scale = new Vector2(1, 1);
_geometry.Path = null;
return;
}
_pathSpriteShape.Offset = LayoutRound(new Vector2((float)(renderOriginX ?? 0), (float)(renderOriginY ?? 0)));
_rectangleVisual.Shapes.Clear();
_rectangleVisual.Shapes.Add(_pathSpriteShape);
_geometry.Path = new CompositionPath(path);
_shape.Scale = scaleX != null && scaleY != null
? new Vector2((float)scaleX.Value, (float)scaleY.Value)
: Vector2.One;
_shape.Offset = LayoutRound(new Vector2((float)(renderOriginX ?? 0), (float)(renderOriginY ?? 0)));
UpdateRender();
}
......@@ -74,36 +71,25 @@ namespace Windows.UI.Xaml.Shapes
private void UpdateFill()
{
if (_pathSpriteShape != null)
{
_fillSubscription.Disposable = null;
_fillSubscription.Disposable = null;
_pathSpriteShape.FillBrush = null;
_shape.FillBrush = null;
_fillSubscription.Disposable =
Brush.AssignAndObserveBrush(Fill, Visual.Compositor, compositionBrush => _pathSpriteShape.FillBrush = compositionBrush);
}
_fillSubscription.Disposable = Brush.AssignAndObserveBrush(Fill, Visual.Compositor, compositionBrush => _shape.FillBrush = compositionBrush);
}
private void UpdateStrokeThickness()
{
if (_pathSpriteShape != null)
{
_pathSpriteShape.StrokeThickness = (float)ActualStrokeThickness;
}
_shape.StrokeThickness = (float)ActualStrokeThickness;
}
private void UpdateStroke()
{
if (_pathSpriteShape != null)
{
_strokeSubscription.Disposable = null;
_strokeSubscription.Disposable = null;
_pathSpriteShape.StrokeBrush = null;
_shape.StrokeBrush = null;
_strokeSubscription.Disposable =
Brush.AssignAndObserveBrush(Stroke, Visual.Compositor, compositionBrush => _pathSpriteShape.StrokeBrush = compositionBrush);
}
_strokeSubscription.Disposable = Brush.AssignAndObserveBrush(Stroke, Visual.Compositor, compositionBrush => _shape.StrokeBrush = compositionBrush);
}
}
}
......@@ -68,6 +68,8 @@ namespace Windows.UI.Xaml
visualTreeRoot.OnElementUnloaded();
}
partial void OnLoading();
// Overloads for the FrameworkElement to raise the events
// (Load/Unload is actually a concept of the FwElement, but it's easier to handle it directly from the UIElement)
private protected virtual void OnFwEltLoading() { }
......@@ -85,6 +87,7 @@ namespace Windows.UI.Xaml
IsLoading = true;
Depth = depth;
OnLoading();
OnFwEltLoading();
// Explicit propagation of the loading even must be performed
......
......@@ -25,7 +25,7 @@ namespace Windows.UI.Xaml
{
public partial class UIElement : DependencyObject
{
private ContainerVisual _visual;
private ShapeVisual _visual;
private Rect _currentFinalRect;
private Rect? _currentClippedFrame;
......@@ -72,16 +72,16 @@ namespace Windows.UI.Xaml
Visual.Opacity = Visibility == Visibility.Visible ? (float)Opacity : 0;
}
internal ContainerVisual Visual
internal ShapeVisual Visual
{
get
{
if (_visual == null)
if (_visual is null)
{
_visual = Window.Current.Compositor.CreateContainerVisual();
_visual = Window.Current.Compositor.CreateShapeVisual();
#if ENABLE_CONTAINER_VISUAL_TRACKING
_visual.Comment = $"Owner:{GetType()}/{(this as FrameworkElement)?.Name}";
_visual.Comment = $"{this.GetDebugDepth():D2}-{this.GetDebugName()}";
#endif
}
......@@ -89,6 +89,16 @@ namespace Windows.UI.Xaml
}
}
#if ENABLE_CONTAINER_VISUAL_TRACKING // Make sure to update the Comment to have the valid depth
partial void OnLoading()
{
if (_visual is not null)
{
_visual.Comment = $"{this.GetDebugDepth():D2}-{this.GetDebugName()}";
}
}
#endif
internal bool ClippingIsSetByCornerRadius { get; set; }
public void AddChild(UIElement child, int? index = null)
......@@ -306,7 +316,20 @@ namespace Windows.UI.Xaml
visual.Size = new Vector2((float)roundedRect.Width, (float)roundedRect.Height);
visual.CenterPoint = new Vector3((float)RenderTransformOrigin.X, (float)RenderTransformOrigin.Y, 0);
ApplyNativeClip(clip ?? Rect.Empty);
// The clipping applied by our parent due to layout constraints are pushed to the visual through the ViewBox property
// This allows special handling of this clipping by the compositor (cf. ShapeVisual.Render).
if (clip is null)
{
visual.ViewBox = null;
}
else
{
var viewBox = visual.Compositor.CreateViewBox();
viewBox.Offset = clip.Value.Location.ToVector2();
viewBox.Size = clip.Value.Size.ToVector2();
visual.ViewBox = viewBox;
}
}
partial void ApplyNativeClip(Rect rect)
......
......@@ -4,22 +4,35 @@ using System.Numerics;
using System.Text;
using Windows.Foundation;
namespace Uno.Extensions
namespace Uno.Extensions;
public static class Vector2Extensions
{
public static class Vector2Extensions
{
/// <summary>
/// Converts a <see cref="Vector2"/> to a <see cref="Point"/>.
/// </summary>
/// <param name="v">The <see cref="Vector2"/> to convert</param>
/// <returns>A <see cref="Point"/></returns>
public static Point ToPoint(this Vector2 v) => new Point(v.X, v.Y);
/// <summary>
/// Converts a <see cref="Vector2"/> to a <see cref="Point"/>.
/// </summary>
/// <param name="vector">The <see cref="Vector2"/> to convert</param>
/// <returns>A <see cref="Point"/></returns>
public static Point ToPoint(this Vector2 vector) => new Point(vector.X, vector.Y);
/// <summary>
/// Converts a <see cref="Point"/> to <see cref="Vector2"/>
/// </summary>
/// <param name="point">The <see cref="Point"/> to Convert</param>
/// <returns>A <see cref="Vector2"/></returns>
public static Vector2 ToVector2(this Point point) => new Vector2((float)point.X, (float)point.Y);
/// <summary>
/// Converts a <see cref="Vector2"/> to <see cref="Size"/>
/// </summary>
/// <param name="vector">The <see cref="Vector2"/> to convert</param>
/// <returns>A <see cref="Size"/></returns>
public static Size ToSize(this Vector2 vector) => new Size(vector.X, vector.Y);
/// <summary>
/// Converts a <see cref="Point"/> to a <see cref="Vector2"/>.
/// </summary>
/// <param name="p">The <see cref="Point"/> to convert</param>
/// <returns>A <see cref="Vector2"/></returns>
public static Vector2 ToVector(this Point p) => new Vector2((float)p.X, (float)p.Y);
}
/// <summary>
/// Converts a <see cref="Size"/> to <see cref="Vector2"/>
/// </summary>
/// <param name="size">The <see cref="Size"/> to Convert</param>
/// <returns>A <see cref="Vector2"/></returns>
public static Vector2 ToVector2(this Size size) => new Vector2((float)size.Width, (float)size.Height);
}
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
......@@ -75,7 +76,8 @@ namespace Windows.UI
/// </summary>
internal double Luminance => (0.299 * _r + 0.587 * _g + 0.114 * _b) / 255;
internal Color WithOpacity(double opacity) => new Color((byte)(_a * opacity), _r, _g, _b);
// Note: This method has an equivalent in Toolkit.ColorExtensions for usage with Windows
internal Color WithOpacity(double opacity) => new((byte)(_a * opacity), _r, _g, _b);
internal uint AsUInt32() => _color;
......
......@@ -705,7 +705,7 @@ namespace Windows.UI.Input
var p2 = _pointer2.RawPosition;
Center = new Point((p1.X + p2.X) / 2, (p1.Y + p2.Y) / 2);
Distance = Vector2.Distance(p1.ToVector(), p2.ToVector());
Distance = Vector2.Distance(p1.ToVector2(), p2.ToVector2());
Angle = Math.Atan2(p1.Y - p2.Y, p1.X - p2.X);
}
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册