From b821aff260a4801a7a9fd30ac47d421d7e0915e5 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 8 Jun 2020 23:01:54 +0200 Subject: [PATCH 01/67] Initial CSS like transform support. --- src/Avalonia.Base/Utilities/MathUtilities.cs | 30 ++ .../LayoutTransformControl.cs | 6 +- .../Animation/Animators/TransformAnimator.cs | 10 +- .../Animators/TransformOperationsAnimator.cs | 35 ++ .../TransformOperationsTransition.cs | 25 + src/Avalonia.Visuals/Matrix.cs | 65 +++ .../Media/IMutableTransform.cs | 12 + src/Avalonia.Visuals/Media/ITransform.cs | 10 + src/Avalonia.Visuals/Media/Transform.cs | 5 +- .../Media/TransformConverter.cs | 23 + .../Transformation/InterpolationUtilities.cs | 40 ++ .../Transformation/TransformOperation.cs | 203 ++++++++ .../Transformation/TransformOperations.cs | 252 ++++++++++ .../Media/Transformation/TransformParser.cs | 463 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 1 + src/Avalonia.Visuals/Visual.cs | 14 +- src/Avalonia.Visuals/VisualTree/IVisual.cs | 2 +- .../Media/MatrixTests.cs | 44 +- .../Media/TransformOperationsTests.cs | 128 +++++ 19 files changed, 1353 insertions(+), 15 deletions(-) create mode 100644 src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs create mode 100644 src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs create mode 100644 src/Avalonia.Visuals/Media/IMutableTransform.cs create mode 100644 src/Avalonia.Visuals/Media/ITransform.cs create mode 100644 src/Avalonia.Visuals/Media/TransformConverter.cs create mode 100644 src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs create mode 100644 src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs create mode 100644 src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs create mode 100644 src/Avalonia.Visuals/Media/Transformation/TransformParser.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index a1a2d6c3d0..042e7f84c6 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -174,5 +174,35 @@ namespace Avalonia.Utilities return val; } } + + /// + /// Converts an angle in degrees to radians. + /// + /// The angle in degrees. + /// The angle in radians. + public static double Deg2Rad(double angle) + { + return angle * (Math.PI / 180d); + } + + /// + /// Converts an angle in gradians to radians. + /// + /// The angle in gradians. + /// The angle in radians. + public static double Grad2Rad(double angle) + { + return angle * (Math.PI / 200d); + } + + /// + /// Converts an angle in turns to radians. + /// + /// The angle in turns. + /// The angle in radians. + public static double Turn2Rad(double angle) + { + return angle * 2 * Math.PI; + } } } diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 8d48f6646d..83ad2b3638 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -14,8 +14,8 @@ namespace Avalonia.Controls /// public class LayoutTransformControl : Decorator { - public static readonly StyledProperty LayoutTransformProperty = - AvaloniaProperty.Register(nameof(LayoutTransform)); + public static readonly StyledProperty LayoutTransformProperty = + AvaloniaProperty.Register(nameof(LayoutTransform)); public static readonly StyledProperty UseRenderTransformProperty = AvaloniaProperty.Register(nameof(LayoutTransform)); @@ -37,7 +37,7 @@ namespace Avalonia.Controls /// /// Gets or sets a graphics transformation that should apply to this element when layout is performed. /// - public Transform LayoutTransform + public ITransform LayoutTransform { get { return GetValue(LayoutTransformProperty); } set { SetValue(LayoutTransformProperty, value); } diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs index 1f1590bdcd..bb1c0da902 100644 --- a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs @@ -1,6 +1,8 @@ using System; +using System.Reactive.Disposables; using Avalonia.Logging; using Avalonia.Media; +using Avalonia.Media.Transformation; namespace Avalonia.Animation.Animators { @@ -19,6 +21,12 @@ namespace Avalonia.Animation.Animators // Check if the Target Property is Transform derived. if (typeof(Transform).IsAssignableFrom(Property.OwnerType)) { + if (ctrl.RenderTransform is TransformOperations) + { + // HACK: This animator cannot reasonably animate CSS transforms at the moment. + return Disposable.Empty; + } + if (ctrl.RenderTransform == null) { var normalTransform = new TransformGroup(); @@ -51,7 +59,7 @@ namespace Avalonia.Animation.Animators // It's a transform object so let's target that. if (renderTransformType == Property.OwnerType) { - return _doubleAnimator.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete); + return _doubleAnimator.Apply(animation, (Transform) ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete); } // It's a TransformGroup and try finding the target there. else if (renderTransformType == typeof(TransformGroup)) diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs new file mode 100644 index 0000000000..f45338122f --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs @@ -0,0 +1,35 @@ +using System; +using Avalonia.Media; +using Avalonia.Media.Transformation; + +namespace Avalonia.Animation.Animators +{ + public class TransformOperationsAnimator : Animator + { + public TransformOperationsAnimator() + { + Validate = ValidateTransform; + } + + private void ValidateTransform(AnimatorKeyFrame kf) + { + if (!(kf.Value is TransformOperations)) + { + throw new InvalidOperationException($"All keyframes must be of type {typeof(TransformOperations)}."); + } + } + + public override ITransform Interpolate(double progress, ITransform oldValue, ITransform newValue) + { + var oldTransform = Cast(oldValue); + var newTransform = Cast(newValue); + + return TransformOperations.Interpolate(oldTransform, newTransform, progress); + } + + private static TransformOperations Cast(ITransform value) + { + return value as TransformOperations ?? TransformOperations.Identity; + } + } +} diff --git a/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs new file mode 100644 index 0000000000..4911b34d91 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs @@ -0,0 +1,25 @@ +using System; +using System.Reactive.Linq; +using Avalonia.Animation.Animators; +using Avalonia.Media; + +namespace Avalonia.Animation +{ + public class TransformOperationsTransition : Transition + { + private static readonly TransformOperationsAnimator _operationsAnimator = new TransformOperationsAnimator(); + + public override IObservable DoTransition(IObservable progress, + ITransform oldValue, + ITransform newValue) + { + return progress + .Select(p => + { + var f = Easing.Ease(p); + + return _operationsAnimator.Interpolate(f, oldValue, newValue); + }); + } + } +} diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index 898c6027a5..e18140aa8d 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -319,5 +319,70 @@ namespace Avalonia ); } } + + public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed) + { + decomposed = default; + + var determinant = matrix.GetDeterminant(); + + if (determinant == 0) + { + return false; + } + + var m11 = matrix.M11; + var m21 = matrix.M21; + var m12 = matrix.M12; + var m22 = matrix.M22; + + // Translation. + decomposed.Translate = new Vector(matrix.M31, matrix.M32); + + // Scale sign. + var scaleX = 1d; + var scaleY = 1d; + + if (determinant < 0) + { + if (m11 < m22) + { + scaleX *= -1d; + } + else + { + scaleY *= -1d; + } + } + + // X Scale. + scaleX *= Math.Sqrt(m11 * m11 + m12 * m12); + + m11 /= scaleX; + m12 /= scaleX; + + // XY Shear. + double scaledShear = m11 * m21 + m12 * m22; + + m21 -= m11 * scaledShear; + m22 -= m12 * scaledShear; + + // Y Scale. + scaleY *= Math.Sqrt(m21 * m21 + m22 * m22); + + decomposed.Scale = new Vector(scaleX, scaleY); + decomposed.Skew = new Vector(scaledShear / scaleY, 0d); + decomposed.Angle = Math.Atan2(m12, m11); + + return true; + } + + public struct Decomposed + { + public Vector Translate; + public Vector Scale; + public Vector Skew; + public double Angle; + } } } diff --git a/src/Avalonia.Visuals/Media/IMutableTransform.cs b/src/Avalonia.Visuals/Media/IMutableTransform.cs new file mode 100644 index 0000000000..2033c434c0 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IMutableTransform.cs @@ -0,0 +1,12 @@ +using System; + +namespace Avalonia.Media +{ + public interface IMutableTransform : ITransform + { + /// + /// Raised when the transform changes. + /// + event EventHandler Changed; + } +} diff --git a/src/Avalonia.Visuals/Media/ITransform.cs b/src/Avalonia.Visuals/Media/ITransform.cs new file mode 100644 index 0000000000..91577fe38e --- /dev/null +++ b/src/Avalonia.Visuals/Media/ITransform.cs @@ -0,0 +1,10 @@ +using System.ComponentModel; + +namespace Avalonia.Media +{ + [TypeConverter(typeof(TransformConverter))] + public interface ITransform + { + Matrix Value { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs index 70ef1eaaf4..7cf1b35ada 100644 --- a/src/Avalonia.Visuals/Media/Transform.cs +++ b/src/Avalonia.Visuals/Media/Transform.cs @@ -8,11 +8,12 @@ namespace Avalonia.Media /// /// Represents a transform on an . /// - public abstract class Transform : Animatable + public abstract class Transform : Animatable, IMutableTransform { static Transform() { - Animation.Animation.RegisterAnimator(prop => typeof(Transform).IsAssignableFrom(prop.OwnerType)); + Animation.Animation.RegisterAnimator(prop => + typeof(ITransform).IsAssignableFrom(prop.OwnerType)); } /// diff --git a/src/Avalonia.Visuals/Media/TransformConverter.cs b/src/Avalonia.Visuals/Media/TransformConverter.cs new file mode 100644 index 0000000000..e79c0b8b7b --- /dev/null +++ b/src/Avalonia.Visuals/Media/TransformConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using Avalonia.Media.Transformation; + +namespace Avalonia.Media +{ + /// + /// Creates an from a string representation. + /// + public class TransformConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return TransformOperations.Parse((string)value); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs new file mode 100644 index 0000000000..1e80eabfc8 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs @@ -0,0 +1,40 @@ +namespace Avalonia.Media.Transformation +{ + internal static class InterpolationUtilities + { + public static double InterpolateScalars(double from, double to, double progress) + { + return from * (1d - progress) + to * progress; + } + + public static Vector InterpolateVectors(Vector from, Vector to, double progress) + { + var x = InterpolateScalars(from.X, to.X, progress); + var y = InterpolateScalars(from.Y, to.Y, progress); + + return new Vector(x, y); + } + + public static Matrix ComposeTransform(Matrix.Decomposed decomposed) + { + // According to https://www.w3.org/TR/css-transforms-1/#recomposing-to-a-2d-matrix + + return Matrix.CreateTranslation(decomposed.Translate) * + Matrix.CreateRotation(decomposed.Angle) * + Matrix.CreateSkew(decomposed.Skew.X, decomposed.Skew.Y) * + Matrix.CreateScale(decomposed.Scale); + } + + public static Matrix.Decomposed InterpolateDecomposedTransforms(ref Matrix.Decomposed from, ref Matrix.Decomposed to, double progres) + { + Matrix.Decomposed result = default; + + result.Translate = InterpolateVectors(from.Translate, to.Translate, progres); + result.Scale = InterpolateVectors(from.Scale, to.Scale, progres); + result.Skew = InterpolateVectors(from.Skew, to.Skew, progres); + result.Angle = InterpolateScalars(from.Angle, to.Angle, progres); + + return result; + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs b/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs new file mode 100644 index 0000000000..cdf31f8e5b --- /dev/null +++ b/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs @@ -0,0 +1,203 @@ +using System.Runtime.InteropServices; + +namespace Avalonia.Media.Transformation +{ + public struct TransformOperation + { + public OperationType Type; + public Matrix Matrix; + public DataLayout Data; + + public enum OperationType + { + Translate, + Rotate, + Scale, + Skew, + Matrix, + Identity + } + + public bool IsIdentity => Matrix.IsIdentity; + + public void Bake() + { + Matrix = Matrix.Identity; + + switch (Type) + { + case OperationType.Translate: + { + Matrix = Matrix.CreateTranslation(Data.Translate.X, Data.Translate.Y); + + break; + } + case OperationType.Rotate: + { + Matrix = Matrix.CreateRotation(Data.Rotate.Angle); + + break; + } + case OperationType.Scale: + { + Matrix = Matrix.CreateScale(Data.Scale.X, Data.Scale.Y); + + break; + } + case OperationType.Skew: + { + Matrix = Matrix.CreateSkew(Data.Skew.X, Data.Skew.Y); + + break; + } + } + } + + public static bool IsOperationIdentity(ref TransformOperation? operation) + { + return !operation.HasValue || operation.Value.IsIdentity; + } + + public static bool TryInterpolate(TransformOperation? from, TransformOperation? to, double progress, + ref TransformOperation result) + { + bool fromIdentity = IsOperationIdentity(ref from); + bool toIdentity = IsOperationIdentity(ref to); + + if (fromIdentity && toIdentity) + { + return true; + } + + TransformOperation fromValue = fromIdentity ? default : from.Value; + TransformOperation toValue = toIdentity ? default : to.Value; + + var interpolationType = toIdentity ? fromValue.Type : toValue.Type; + + result.Type = interpolationType; + + switch (interpolationType) + { + case OperationType.Translate: + { + double fromX = fromIdentity ? 0 : fromValue.Data.Translate.X; + double fromY = fromIdentity ? 0 : fromValue.Data.Translate.Y; + + double toX = toIdentity ? 0 : toValue.Data.Translate.X; + double toY = toIdentity ? 0 : toValue.Data.Translate.Y; + + result.Data.Translate.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); + result.Data.Translate.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); + + result.Bake(); + + break; + } + case OperationType.Rotate: + { + double fromAngle = fromIdentity ? 0 : fromValue.Data.Rotate.Angle; + + double toAngle = toIdentity ? 0 : toValue.Data.Rotate.Angle; + + result.Data.Rotate.Angle = InterpolationUtilities.InterpolateScalars(fromAngle, toAngle, progress); + + result.Bake(); + + break; + } + case OperationType.Scale: + { + double fromX = fromIdentity ? 1 : fromValue.Data.Scale.X; + double fromY = fromIdentity ? 1 : fromValue.Data.Scale.Y; + + double toX = toIdentity ? 1 : toValue.Data.Scale.X; + double toY = toIdentity ? 1 : toValue.Data.Scale.Y; + + result.Data.Scale.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); + result.Data.Scale.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); + + result.Bake(); + + break; + } + case OperationType.Skew: + { + double fromX = fromIdentity ? 0 : fromValue.Data.Skew.X; + double fromY = fromIdentity ? 0 : fromValue.Data.Skew.Y; + + double toX = toIdentity ? 0 : toValue.Data.Skew.X; + double toY = toIdentity ? 0 : toValue.Data.Skew.Y; + + result.Data.Skew.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); + result.Data.Skew.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); + + result.Bake(); + + break; + } + case OperationType.Matrix: + { + var fromMatrix = fromIdentity ? Matrix.Identity : fromValue.Matrix; + var toMatrix = toIdentity ? Matrix.Identity : toValue.Matrix; + + if (!Matrix.TryDecomposeTransform(fromMatrix, out Matrix.Decomposed fromDecomposed) || + !Matrix.TryDecomposeTransform(toMatrix, out Matrix.Decomposed toDecomposed)) + { + return false; + } + + var interpolated = + InterpolationUtilities.InterpolateDecomposedTransforms( + ref fromDecomposed, ref toDecomposed, + progress); + + result.Matrix = InterpolationUtilities.ComposeTransform(interpolated); + + break; + } + case OperationType.Identity: + { + // Do nothing. + break; + } + } + + return true; + } + + [StructLayout(LayoutKind.Explicit)] + public struct DataLayout + { + [FieldOffset(0)] public SkewLayout Skew; + + [FieldOffset(0)] public ScaleLayout Scale; + + [FieldOffset(0)] public TranslateLayout Translate; + + [FieldOffset(0)] public RotateLayout Rotate; + + public struct SkewLayout + { + public double X; + public double Y; + } + + public struct ScaleLayout + { + public double X; + public double Y; + } + + public struct TranslateLayout + { + public double X; + public double Y; + } + + public struct RotateLayout + { + public double Angle; + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs b/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs new file mode 100644 index 0000000000..9f711a2d63 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Avalonia.Media.Transformation +{ + public sealed class TransformOperations : ITransform + { + public static TransformOperations Identity { get; } = new TransformOperations(new List()); + + private readonly List _operations; + + private TransformOperations(List operations) + { + _operations = operations ?? throw new ArgumentNullException(nameof(operations)); + + IsIdentity = CheckIsIdentity(); + + Value = ApplyTransforms(); + } + + public bool IsIdentity { get; } + + public IReadOnlyList Operations => _operations; + + public Matrix Value { get; } + + public static TransformOperations Parse(string s) + { + return TransformParser.Parse(s); + } + + public static Builder CreateBuilder(int capacity) + { + return new Builder(capacity); + } + + public static TransformOperations Interpolate(TransformOperations from, TransformOperations to, double progress) + { + TransformOperations result = Identity; + + if (!TryInterpolate(from, to, progress, ref result)) + { + // If the matrices cannot be interpolated, fallback to discrete animation logic. + // See https://drafts.csswg.org/css-transforms/#matrix-interpolation + result = progress < 0.5 ? from : to; + } + + return result; + } + + private Matrix ApplyTransforms(int startOffset = 0) + { + Matrix matrix = Matrix.Identity; + + for (var i = startOffset; i < _operations.Count; i++) + { + TransformOperation operation = _operations[i]; + matrix *= operation.Matrix; + } + + return matrix; + } + + private bool CheckIsIdentity() + { + foreach (TransformOperation operation in _operations) + { + if (!operation.IsIdentity) + { + return false; + } + } + + return true; + } + + private static bool TryInterpolate(TransformOperations from, TransformOperations to, double progress, ref TransformOperations result) + { + bool fromIdentity = from.IsIdentity; + bool toIdentity = to.IsIdentity; + + if (fromIdentity && toIdentity) + { + return true; + } + + int matchingPrefixLength = ComputeMatchingPrefixLength(from, to); + int fromSize = fromIdentity ? 0 : from._operations.Count; + int toSize = toIdentity ? 0 : to._operations.Count; + int numOperations = Math.Max(fromSize, toSize); + + var builder = new Builder(matchingPrefixLength); + + for (int i = 0; i < matchingPrefixLength; i++) + { + TransformOperation interpolated = new TransformOperation + { + Type = TransformOperation.OperationType.Identity + }; + + if (!TransformOperation.TryInterpolate( + i >= fromSize ? default(TransformOperation?) : from._operations[i], + i >= toSize ? default(TransformOperation?) : to._operations[i], + progress, + ref interpolated)) + { + return false; + } + + builder.Append(interpolated); + } + + if (matchingPrefixLength < numOperations) + { + if (!ComputeDecomposedTransform(from, matchingPrefixLength, out Matrix.Decomposed fromDecomposed) || + !ComputeDecomposedTransform(to, matchingPrefixLength, out Matrix.Decomposed toDecomposed)) + { + return false; + } + + var transform = InterpolationUtilities.InterpolateDecomposedTransforms(ref fromDecomposed, ref toDecomposed, progress); + + builder.AppendMatrix(InterpolationUtilities.ComposeTransform(transform)); + } + + result = builder.Build(); + + return true; + } + + private static bool ComputeDecomposedTransform(TransformOperations operations, int startOffset, out Matrix.Decomposed decomposed) + { + Matrix transform = operations.ApplyTransforms(startOffset); + + if (!Matrix.TryDecomposeTransform(transform, out decomposed)) + { + return false; + } + + return true; + } + + private static int ComputeMatchingPrefixLength(TransformOperations from, TransformOperations to) + { + int numOperations = Math.Min(from._operations.Count, to._operations.Count); + + for (int i = 0; i < numOperations; i++) + { + if (from._operations[i].Type != to._operations[i].Type) + { + return i; + } + } + + // If the operations match to the length of the shorter list, then pad its + // length with the matching identity operations. + // https://drafts.csswg.org/css-transforms/#transform-function-lists + return Math.Max(from._operations.Count, to._operations.Count); + } + + public readonly struct Builder + { + private readonly List _operations; + + public Builder(int capacity) + { + _operations = new List(capacity); + } + + public void AppendTranslate(double x, double y) + { + var toAdd = new TransformOperation(); + + toAdd.Type = TransformOperation.OperationType.Translate; + toAdd.Data.Translate.X = x; + toAdd.Data.Translate.Y = y; + + toAdd.Bake(); + + _operations.Add(toAdd); + } + + public void AppendRotate(double angle) + { + var toAdd = new TransformOperation(); + + toAdd.Type = TransformOperation.OperationType.Rotate; + toAdd.Data.Rotate.Angle = angle; + + toAdd.Bake(); + + _operations.Add(toAdd); + } + + public void AppendScale(double x, double y) + { + var toAdd = new TransformOperation(); + + toAdd.Type = TransformOperation.OperationType.Scale; + toAdd.Data.Scale.X = x; + toAdd.Data.Scale.Y = y; + + toAdd.Bake(); + + _operations.Add(toAdd); + } + + public void AppendSkew(double x, double y) + { + var toAdd = new TransformOperation(); + + toAdd.Type = TransformOperation.OperationType.Skew; + toAdd.Data.Skew.X = x; + toAdd.Data.Skew.Y = y; + + toAdd.Bake(); + + _operations.Add(toAdd); + } + + public void AppendMatrix(Matrix matrix) + { + var toAdd = new TransformOperation(); + + toAdd.Type = TransformOperation.OperationType.Matrix; + toAdd.Matrix = matrix; + + _operations.Add(toAdd); + } + + public void AppendIdentity() + { + var toAdd = new TransformOperation(); + + toAdd.Type = TransformOperation.OperationType.Identity; + + _operations.Add(toAdd); + } + + public void Append(TransformOperation toAdd) + { + _operations.Add(toAdd); + } + + public TransformOperations Build() + { + return new TransformOperations(_operations); + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs b/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs new file mode 100644 index 0000000000..2a3912832b --- /dev/null +++ b/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs @@ -0,0 +1,463 @@ +using System; +using System.Globalization; +using Avalonia.Utilities; + +namespace Avalonia.Media.Transformation +{ + public static class TransformParser + { + private static readonly (string, TransformFunction)[] s_functionMapping = + { + ("translate", TransformFunction.Translate), + ("translateX", TransformFunction.TranslateX), + ("translateY", TransformFunction.TranslateY), + ("scale", TransformFunction.Scale), + ("scaleX", TransformFunction.ScaleX), + ("scaleY", TransformFunction.ScaleY), + ("skew", TransformFunction.Skew), + ("skewX", TransformFunction.SkewX), + ("skewY", TransformFunction.SkewY), + ("rotate", TransformFunction.Rotate), + ("matrix", TransformFunction.Matrix) + }; + + private static readonly (string, Unit)[] s_unitMapping = + { + ("deg", Unit.Degree), + ("grad", Unit.Gradian), + ("rad", Unit.Radian), + ("turn", Unit.Turn), + ("px", Unit.Pixel) + }; + + public static TransformOperations Parse(string s) + { + void ThrowInvalidFormat() + { + throw new FormatException($"Invalid transform string: '{s}'."); + } + + if (string.IsNullOrEmpty(s)) + { + throw new ArgumentException(nameof(s)); + } + + var span = s.AsSpan().Trim(); + + if (span.Equals("none".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return TransformOperations.Identity; + } + + var builder = TransformOperations.CreateBuilder(0); + + while (true) + { + var beginIndex = span.IndexOf('('); + var endIndex = span.IndexOf(')'); + + if (beginIndex == -1 || endIndex == -1) + { + ThrowInvalidFormat(); + } + + var namePart = span.Slice(0, beginIndex).Trim(); + + var function = ParseTransformFunction(in namePart); + + if (function == TransformFunction.Invalid) + { + ThrowInvalidFormat(); + } + + var valuePart = span.Slice(beginIndex + 1, endIndex - beginIndex - 1).Trim(); + + ParseFunction(in valuePart, function, in builder); + + span = span.Slice(endIndex + 1); + + if (span.IsWhiteSpace()) + { + break; + } + } + + return builder.Build(); + } + + private static void ParseFunction( + in ReadOnlySpan functionPart, + TransformFunction function, + in TransformOperations.Builder builder) + { + static UnitValue ParseValue(ReadOnlySpan part) + { + int unitIndex = -1; + + for (int i = 0; i < part.Length; i++) + { + char c = part[i]; + + if (char.IsDigit(c) || c == '-' || c == '.') + { + continue; + } + + unitIndex = i; + break; + } + + Unit unit = Unit.None; + + if (unitIndex != -1) + { + var unitPart = part.Slice(unitIndex, part.Length - unitIndex); + + unit = ParseUnit(unitPart); + + part = part.Slice(0, unitIndex); + } + + var value = double.Parse(part.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture); + + return new UnitValue(unit, value); + } + + static int ParseValuePair( + in ReadOnlySpan part, + ref UnitValue leftValue, + ref UnitValue rightValue) + { + var commaIndex = part.IndexOf(','); + + if (commaIndex != -1) + { + var leftPart = part.Slice(0, commaIndex).Trim(); + var rightPart = part.Slice(commaIndex + 1, part.Length - commaIndex - 1).Trim(); + + leftValue = ParseValue(leftPart); + rightValue = ParseValue(rightPart); + + return 2; + } + + leftValue = ParseValue(part); + + return 1; + } + + static int ParseCommaDelimitedValues(ReadOnlySpan part, in Span outValues) + { + int valueIndex = 0; + + while (true) + { + if (valueIndex >= outValues.Length) + { + throw new FormatException("Too many provided values."); + } + + var commaIndex = part.IndexOf(','); + + if (commaIndex == -1) + { + if (!part.IsWhiteSpace()) + { + outValues[valueIndex++] = ParseValue(part); + } + + break; + } + + var valuePart = part.Slice(0, commaIndex).Trim(); + + outValues[valueIndex++] = ParseValue(valuePart); + + part = part.Slice(commaIndex + 1, part.Length - commaIndex - 1); + } + + return valueIndex; + } + + switch (function) + { + case TransformFunction.Scale: + case TransformFunction.ScaleX: + case TransformFunction.ScaleY: + { + var scaleX = UnitValue.One; + var scaleY = UnitValue.One; + + int count = ParseValuePair(functionPart, ref scaleX, ref scaleY); + + if (count != 1 && (function == TransformFunction.ScaleX || function == TransformFunction.ScaleY)) + { + ThrowFormatInvalidValueCount(function, 1); + } + + VerifyZeroOrUnit(function, in scaleX, Unit.None); + VerifyZeroOrUnit(function, in scaleY, Unit.None); + + if (function == TransformFunction.ScaleX) + { + scaleY = UnitValue.Zero; + } + else if (function == TransformFunction.ScaleY) + { + scaleY = scaleX; + scaleX = UnitValue.Zero; + } + else if (count == 1) + { + scaleY = scaleX; + } + + builder.AppendScale(scaleX.Value, scaleY.Value); + + break; + } + case TransformFunction.Skew: + case TransformFunction.SkewX: + case TransformFunction.SkewY: + { + var skewX = UnitValue.Zero; + var skewY = UnitValue.Zero; + + int count = ParseValuePair(functionPart, ref skewX, ref skewY); + + if (count != 1 && (function == TransformFunction.SkewX || function == TransformFunction.SkewY)) + { + ThrowFormatInvalidValueCount(function, 1); + } + + VerifyZeroOrAngle(function, in skewX); + VerifyZeroOrAngle(function, in skewY); + + if (function == TransformFunction.SkewX) + { + skewY = UnitValue.Zero; + } + else if (function == TransformFunction.SkewY) + { + skewY = skewX; + skewX = UnitValue.Zero; + } + else if (count == 1) + { + skewY = skewX; + } + + builder.AppendSkew(ToRadians(in skewX), ToRadians(in skewY)); + + break; + } + case TransformFunction.Rotate: + { + var angle = UnitValue.Zero; + UnitValue _ = default; + + int count = ParseValuePair(functionPart, ref angle, ref _); + + if (count != 1) + { + ThrowFormatInvalidValueCount(function, 1); + } + + VerifyZeroOrAngle(function, in angle); + + builder.AppendRotate(ToRadians(in angle)); + + break; + } + case TransformFunction.Translate: + case TransformFunction.TranslateX: + case TransformFunction.TranslateY: + { + var translateX = UnitValue.Zero; + var translateY = UnitValue.Zero; + + int count = ParseValuePair(functionPart, ref translateX, ref translateY); + + if (count != 1 && (function == TransformFunction.TranslateX || function == TransformFunction.TranslateY)) + { + ThrowFormatInvalidValueCount(function, 1); + } + + VerifyZeroOrUnit(function, in translateX, Unit.Pixel); + VerifyZeroOrUnit(function, in translateY, Unit.Pixel); + + if (function == TransformFunction.TranslateX) + { + translateY = UnitValue.Zero; + } + else if (function == TransformFunction.TranslateY) + { + translateY = translateX; + translateX = UnitValue.Zero; + } + else if (count == 1) + { + translateY = translateX; + } + + builder.AppendTranslate(translateX.Value, translateY.Value); + + break; + } + case TransformFunction.Matrix: + { + Span values = stackalloc UnitValue[6]; + + int count = ParseCommaDelimitedValues(functionPart, in values); + + if (count != 6) + { + ThrowFormatInvalidValueCount(function, 6); + } + + foreach (UnitValue value in values) + { + VerifyZeroOrUnit(function, value, Unit.None); + } + + var matrix = new Matrix( + values[0].Value, + values[1].Value, + values[2].Value, + values[3].Value, + values[4].Value, + values[5].Value); + + builder.AppendMatrix(matrix); + + break; + } + } + } + + private static void VerifyZeroOrUnit(TransformFunction function, in UnitValue value, Unit unit) + { + bool isZero = value.Unit == Unit.None && value.Value == 0d; + + if (!isZero && value.Unit != unit) + { + ThrowFormatInvalidValue(function, in value); + } + } + + private static void VerifyZeroOrAngle(TransformFunction function, in UnitValue value) + { + if (value.Value != 0d && !IsAngleUnit(value.Unit)) + { + ThrowFormatInvalidValue(function, in value); + } + } + + private static bool IsAngleUnit(Unit unit) + { + switch (unit) + { + case Unit.Radian: + case Unit.Degree: + case Unit.Turn: + { + return true; + } + } + + return false; + } + + private static void ThrowFormatInvalidValue(TransformFunction function, in UnitValue value) + { + var unitString = value.Unit == Unit.None ? string.Empty : value.Unit.ToString(); + + throw new FormatException($"Invalid value {value.Value} {unitString} for {function}"); + } + + private static void ThrowFormatInvalidValueCount(TransformFunction function, int count) + { + throw new FormatException($"Invalid format. {function} expects {count} value(s)."); + } + + private static Unit ParseUnit(in ReadOnlySpan part) + { + foreach (var (name, unit) in s_unitMapping) + { + if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return unit; + } + } + + throw new FormatException($"Invalid unit: {part.ToString()}"); + } + + private static TransformFunction ParseTransformFunction(in ReadOnlySpan part) + { + foreach (var (name, transformFunction) in s_functionMapping) + { + if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return transformFunction; + } + } + + return TransformFunction.Invalid; + } + + private static double ToRadians(in UnitValue value) + { + return value.Unit switch + { + Unit.Radian => value.Value, + Unit.Gradian => MathUtilities.Grad2Rad(value.Value), + Unit.Degree => MathUtilities.Deg2Rad(value.Value), + Unit.Turn => MathUtilities.Turn2Rad(value.Value), + _ => value.Value + }; + } + + private enum Unit + { + None, + Pixel, + Radian, + Gradian, + Degree, + Turn + } + + private readonly struct UnitValue + { + public readonly Unit Unit; + public readonly double Value; + + public UnitValue(Unit unit, double value) + { + Unit = unit; + Value = value; + } + + public static UnitValue Zero => new UnitValue(Unit.None, 0); + + public static UnitValue One => new UnitValue(Unit.None, 1); + } + + private enum TransformFunction + { + Invalid, + Translate, + TranslateX, + TranslateY, + Scale, + ScaleX, + ScaleY, + Skew, + SkewX, + SkewY, + Rotate, + Matrix + } + } +} diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs index 6cd6442095..5d802c27b9 100644 --- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs @@ -6,6 +6,7 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Transformation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")] diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index bb9a4cf208..cd6e5bb075 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -68,8 +68,8 @@ namespace Avalonia /// /// Defines the property. /// - public static readonly StyledProperty RenderTransformProperty = - AvaloniaProperty.Register(nameof(RenderTransform)); + public static readonly StyledProperty RenderTransformProperty = + AvaloniaProperty.Register(nameof(RenderTransform)); /// /// Defines the property. @@ -219,7 +219,7 @@ namespace Avalonia /// /// Gets the render transform of the control. /// - public Transform RenderTransform + public ITransform RenderTransform { get { return GetValue(RenderTransformProperty); } set { SetValue(RenderTransformProperty, value); } @@ -391,9 +391,9 @@ namespace Avalonia _visualRoot = e.Root; - if (RenderTransform != null) + if (RenderTransform is IMutableTransform mutableTransform) { - RenderTransform.Changed += RenderTransformChanged; + mutableTransform.Changed += RenderTransformChanged; } EnableTransitions(); @@ -428,9 +428,9 @@ namespace Avalonia _visualRoot = null; - if (RenderTransform != null) + if (RenderTransform is IMutableTransform mutableTransform) { - RenderTransform.Changed -= RenderTransformChanged; + mutableTransform.Changed -= RenderTransformChanged; } DisableTransitions(); diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs index 6f905cc269..50787655d9 100644 --- a/src/Avalonia.Visuals/VisualTree/IVisual.cs +++ b/src/Avalonia.Visuals/VisualTree/IVisual.cs @@ -76,7 +76,7 @@ namespace Avalonia.VisualTree /// /// Gets or sets the render transform of the control. /// - Transform RenderTransform { get; set; } + ITransform RenderTransform { get; set; } /// /// Gets or sets the render transform origin of the control. diff --git a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs index ff1d17164e..44e2e8663b 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Avalonia.Utilities; using Xunit; namespace Avalonia.Visuals.UnitTests.Media @@ -12,5 +13,46 @@ namespace Avalonia.Visuals.UnitTests.Media var expected = new Matrix(1, 2, 3, -4, 5, 6); Assert.Equal(expected, matrix); } + + [Fact] + public void Can_Decompose_Translation() + { + var matrix = Matrix.CreateTranslation(5, 10); + + var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed); + + Assert.Equal(true, result); + Assert.Equal(5, decomposed.Translate.X); + Assert.Equal(10, decomposed.Translate.Y); + } + + [Fact] + public void Can_Decompose_Angle() + { + var angleRad = MathUtilities.Deg2Rad(30); + + var matrix = Matrix.CreateRotation(angleRad); + + var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed); + + Assert.Equal(true, result); + Assert.Equal(angleRad, decomposed.Angle); + } + + [Theory] + [InlineData(1d, 1d)] + [InlineData(-1d, 1d)] + [InlineData(1d, -1d)] + [InlineData(5d, 10d)] + public void Can_Decompose_Scale(double x, double y) + { + var matrix = Matrix.CreateScale(x, y); + + var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed); + + Assert.Equal(true, result); + Assert.Equal(x, decomposed.Scale.X); + Assert.Equal(y, decomposed.Scale.Y); + } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs new file mode 100644 index 0000000000..8e0520a71d --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs @@ -0,0 +1,128 @@ +using Avalonia.Media.Transformation; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class TransformOperationsTests + { + [Fact] + public void Can_Parse_Compound_Operations() + { + var data = "scale(1,2) translate(3px,4px) rotate(5deg) skew(6deg,7deg)"; + + var transform = TransformOperations.Parse(data); + + var operations = transform.Operations; + + Assert.Equal(TransformOperation.OperationType.Scale, operations[0].Type); + Assert.Equal(1, operations[0].Data.Scale.X); + Assert.Equal(2, operations[0].Data.Scale.Y); + + Assert.Equal(TransformOperation.OperationType.Translate, operations[1].Type); + Assert.Equal(3, operations[1].Data.Translate.X); + Assert.Equal(4, operations[1].Data.Translate.Y); + + Assert.Equal(TransformOperation.OperationType.Rotate, operations[2].Type); + Assert.Equal(MathUtilities.Deg2Rad(5), operations[2].Data.Rotate.Angle); + + Assert.Equal(TransformOperation.OperationType.Skew, operations[3].Type); + Assert.Equal(MathUtilities.Deg2Rad(6), operations[3].Data.Skew.X); + Assert.Equal(MathUtilities.Deg2Rad(7), operations[3].Data.Skew.Y); + } + + [Fact] + public void Can_Parse_Matrix_Operation() + { + var data = "matrix(1,2,3,4,5,6)"; + + var transform = TransformOperations.Parse(data); + } + + [Theory] + [InlineData(0d, 10d, 0d)] + [InlineData(0.5d, 5d, 10d)] + [InlineData(1d, 0d, 20d)] + public void Can_Interpolate_Translation(double progress, double x, double y) + { + var from = TransformOperations.Parse("translateX(10px)"); + var to = TransformOperations.Parse("translateY(20px)"); + + var interpolated = TransformOperations.Interpolate(from, to, progress); + + var operations = interpolated.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Translate, operations[0].Type); + Assert.Equal(x, operations[0].Data.Translate.X); + Assert.Equal(y, operations[0].Data.Translate.Y); + } + + [Theory] + [InlineData(0d, 10d, 0d)] + [InlineData(0.5d, 5d, 10d)] + [InlineData(1d, 0d, 20d)] + public void Can_Interpolate_Scale(double progress, double x, double y) + { + var from = TransformOperations.Parse("scaleX(10)"); + var to = TransformOperations.Parse("scaleY(20)"); + + var interpolated = TransformOperations.Interpolate(from, to, progress); + + var operations = interpolated.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Scale, operations[0].Type); + Assert.Equal(x, operations[0].Data.Scale.X); + Assert.Equal(y, operations[0].Data.Scale.Y); + } + + [Theory] + [InlineData(0d, 10d, 0d)] + [InlineData(0.5d, 5d, 10d)] + [InlineData(1d, 0d, 20d)] + public void Can_Interpolate_Skew(double progress, double x, double y) + { + var from = TransformOperations.Parse("skewX(10deg)"); + var to = TransformOperations.Parse("skewY(20deg)"); + + var interpolated = TransformOperations.Interpolate(from, to, progress); + + var operations = interpolated.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Skew, operations[0].Type); + Assert.Equal(MathUtilities.Deg2Rad(x), operations[0].Data.Skew.X); + Assert.Equal(MathUtilities.Deg2Rad(y), operations[0].Data.Skew.Y); + } + + [Theory] + [InlineData(0d, 10d)] + [InlineData(0.5d, 15d)] + [InlineData(1d,20d)] + public void Can_Interpolate_Rotation(double progress, double angle) + { + var from = TransformOperations.Parse("rotate(10deg)"); + var to = TransformOperations.Parse("rotate(20deg)"); + + var interpolated = TransformOperations.Interpolate(from, to, progress); + + var operations = interpolated.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Rotate, operations[0].Type); + Assert.Equal(MathUtilities.Deg2Rad(angle), operations[0].Data.Rotate.Angle); + } + + [Fact] + public void Can_Interpolate_Matrix() + { + double progress = 0.5d; + + var from = TransformOperations.Parse("rotate(45deg)"); + var to = TransformOperations.Parse("translate(100px, 100px) rotate(1215deg)"); + + var interpolated = TransformOperations.Interpolate(from, to, progress); + } + } +} From efa17b83afa7e1f303720ea809d76ca06969cc29 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 9 Jun 2020 12:45:42 +0200 Subject: [PATCH 02/67] Use epsilon when checking for singular matrices. Add benchmark for decomposing a Matrix. Add more tests for Matrix struct. --- src/Avalonia.Base/Utilities/MathUtilities.cs | 1 - src/Avalonia.Visuals/Matrix.cs | 11 ++-- .../Visuals/MatrixBenchmarks.cs | 16 ++++++ .../Media/MatrixTests.cs | 53 ++++++++++++++++--- 4 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 042e7f84c6..d4d2bf16b8 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.InteropServices; namespace Avalonia.Utilities { diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index e18140aa8d..3c8e5e39f2 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -9,6 +9,8 @@ namespace Avalonia /// public readonly struct Matrix : IEquatable { + private const float DecomposeEpsilon = 0.0001f; + private readonly double _m11; private readonly double _m12; private readonly double _m21; @@ -54,7 +56,7 @@ namespace Avalonia /// /// HasInverse Property - returns true if this matrix is invertible, false otherwise. /// - public bool HasInverse => GetDeterminant() != 0; + public bool HasInverse => Math.Abs(GetDeterminant()) >= double.Epsilon; /// /// The first element of the first row @@ -286,7 +288,7 @@ namespace Avalonia { double d = GetDeterminant(); - if (d == 0) + if (Math.Abs(d) < double.Epsilon) { throw new InvalidOperationException("Transform is not invertible."); } @@ -325,8 +327,9 @@ namespace Avalonia decomposed = default; var determinant = matrix.GetDeterminant(); - - if (determinant == 0) + + // Based upon constant in System.Numerics.Matrix4x4. + if (Math.Abs(determinant) < DecomposeEpsilon) { return false; } diff --git a/tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs b/tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs new file mode 100644 index 0000000000..17e2237eb0 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs @@ -0,0 +1,16 @@ +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Visuals +{ + [MemoryDiagnoser, InProcess] + public class MatrixBenchmarks + { + private static readonly Matrix s_data = Matrix.Identity; + + [Benchmark(Baseline = true)] + public bool Decompose() + { + return Matrix.TryDecomposeTransform(s_data, out Matrix.Decomposed decomposed); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs index 44e2e8663b..6ef48b6161 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System; using Avalonia.Utilities; using Xunit; @@ -7,13 +7,29 @@ namespace Avalonia.Visuals.UnitTests.Media public class MatrixTests { [Fact] - public void Parse_Parses() + public void Can_Parse() { var matrix = Matrix.Parse("1,2,3,-4,5 6"); var expected = new Matrix(1, 2, 3, -4, 5, 6); Assert.Equal(expected, matrix); } + [Fact] + public void Singular_Has_No_Inverse() + { + var matrix = new Matrix(0, 0, 0, 0, 0, 0); + + Assert.False(matrix.HasInverse); + } + + [Fact] + public void Identity_Has_Inverse() + { + var matrix = Matrix.Identity; + + Assert.True(matrix.HasInverse); + } + [Fact] public void Can_Decompose_Translation() { @@ -26,17 +42,25 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(10, decomposed.Translate.Y); } - [Fact] - public void Can_Decompose_Angle() + [Theory] + [InlineData(30d)] + [InlineData(0d)] + [InlineData(90d)] + [InlineData(270d)] + public void Can_Decompose_Angle(double angleDeg) { - var angleRad = MathUtilities.Deg2Rad(30); + var angleRad = MathUtilities.Deg2Rad(angleDeg); var matrix = Matrix.CreateRotation(angleRad); var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed); Assert.Equal(true, result); - Assert.Equal(angleRad, decomposed.Angle); + + var expected = NormalizeAngle(angleRad); + var actual = NormalizeAngle(decomposed.Angle); + + Assert.Equal(expected, actual, 4); } [Theory] @@ -54,5 +78,22 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(x, decomposed.Scale.X); Assert.Equal(y, decomposed.Scale.Y); } + + private static double NormalizeAngle(double rad) + { + double twoPi = 2 * Math.PI; + + while (rad < 0) + { + rad += twoPi; + } + + while (rad > twoPi) + { + rad -= twoPi; + } + + return rad; + } } } From 3f1f94f5045f39bc9a81b3345fb571d875d11bb3 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 9 Jun 2020 12:51:06 +0200 Subject: [PATCH 03/67] Implement remainder of matrix interpolation fallback test. --- .../Media/TransformOperationsTests.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs index 8e0520a71d..8b4ccba57d 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs @@ -115,7 +115,7 @@ namespace Avalonia.Visuals.UnitTests.Media } [Fact] - public void Can_Interpolate_Matrix() + public void Interpolation_Fallback_To_Matrix() { double progress = 0.5d; @@ -123,6 +123,11 @@ namespace Avalonia.Visuals.UnitTests.Media var to = TransformOperations.Parse("translate(100px, 100px) rotate(1215deg)"); var interpolated = TransformOperations.Interpolate(from, to, progress); + + var operations = interpolated.Operations; + + Assert.Single(operations); + Assert.Equal(TransformOperation.OperationType.Matrix, operations[0].Type); } } } From eeada89f5b74106ecde56a68e68b9abb848bd432 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 20 May 2020 13:53:38 -0300 Subject: [PATCH 04/67] Add WndProcs to handle extended frame windows. --- .../Interop/UnmanagedMethods.cs | 3 + .../Avalonia.Win32/WindowImpl.WndProc.cs | 155 +++++++++++++++++- 2 files changed, 157 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index ba3775200b..b66de8e0a8 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1311,6 +1311,9 @@ namespace Avalonia.Win32.Interop [DllImport("dwmapi.dll")] public static extern int DwmIsCompositionEnabled(out bool enabled); + [DllImport("dwmapi.dll")] + public static extern bool DwmDefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult); + [DllImport("dwmapi.dll")] public static extern void DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 138553b962..82374893f1 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -12,11 +12,164 @@ using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 { + public partial class WindowImpl + { + private int LEFTEXTENDWIDTH = 4; + private int RIGHTEXTENDWIDTH = 4; + private int BOTTOMEXTENDWIDTH = 4; + private int TOPEXTENDWIDTH = 100; + + // Hit test the frame for resizing and moving. + HitTestValues HitTestNCA(IntPtr hWnd, IntPtr wParam, IntPtr lParam) + { + // Get the point coordinates for the hit test. + var ptMouse = PointFromLParam(lParam); + + // Get the window rectangle. + GetWindowRect(hWnd, out var rcWindow); + + // Get the frame rectangle, adjusted for the style without a caption. + RECT rcFrame = new RECT(); + AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0); + + // Determine if the hit test is for resizing. Default middle (1,1). + ushort uRow = 1; + ushort uCol = 1; + bool fOnResizeBorder = false; + + // Determine if the point is at the top or bottom of the window. + if (ptMouse.Y >= rcWindow.top && ptMouse.Y < rcWindow.top + TOPEXTENDWIDTH) + { + fOnResizeBorder = (ptMouse.Y < (rcWindow.top - rcFrame.top)); + uRow = 0; + } + else if (ptMouse.Y < rcWindow.bottom && ptMouse.Y >= rcWindow.bottom - BOTTOMEXTENDWIDTH) + { + uRow = 2; + } + + // Determine if the point is at the left or right of the window. + if (ptMouse.X >= rcWindow.left && ptMouse.X < rcWindow.left + LEFTEXTENDWIDTH) + { + uCol = 0; // left side + } + else if (ptMouse.X < rcWindow.right && ptMouse.X >= rcWindow.right - RIGHTEXTENDWIDTH) + { + uCol = 2; // right side + } + + // Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT) + HitTestValues[][] hitTests = new[] + { + new []{ HitTestValues.HTTOPLEFT, fOnResizeBorder ? HitTestValues.HTTOP : HitTestValues.HTCAPTION, HitTestValues.HTTOPRIGHT }, + new []{ HitTestValues.HTLEFT, HitTestValues.HTNOWHERE, HitTestValues.HTRIGHT }, + new []{ HitTestValues.HTBOTTOMLEFT, HitTestValues.HTBOTTOM, HitTestValues.HTBOTTOMRIGHT }, + }; + + System.Diagnostics.Debug.WriteLine(hitTests[uRow][uCol]); + + return hitTests[uRow][uCol]; + } + + protected virtual unsafe IntPtr CustomCaptionProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool callDwp) + { + IntPtr lRet = IntPtr.Zero; + + callDwp = !DwmDefWindowProc(hWnd, msg, wParam, lParam, ref lRet); + + MARGINS margins = new MARGINS { cxLeftWidth = 0, cxRightWidth = 0, cyBottomHeight = 0, cyTopHeight = 100 }; + RECT border_thickness = new RECT(); + + switch ((WindowsMessage)msg) + { + case WindowsMessage.WM_ACTIVATE: + { + if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME)) + { + AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0); + border_thickness.left *= -1; + border_thickness.top *= -1; + } + else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) + { + border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; + } + + LEFTEXTENDWIDTH = border_thickness.left; + RIGHTEXTENDWIDTH = border_thickness.right; + BOTTOMEXTENDWIDTH = border_thickness.bottom; + + // Extend the frame into the client area. + margins.cxLeftWidth = border_thickness.left; + margins.cxRightWidth = border_thickness.right; + margins.cyBottomHeight = border_thickness.bottom; + margins.cyTopHeight = border_thickness.top; + + var hr = DwmExtendFrameIntoClientArea(hWnd, ref margins); + + //if (hr < 0) + { + // Handle the error. + } + + lRet = IntPtr.Zero; + callDwp = true; + break; + } + + case WindowsMessage.WM_NCCALCSIZE: + { + if (ToInt32(wParam) == 1) + { + lRet = IntPtr.Zero; + callDwp = false; + } + break; + } + + case WindowsMessage.WM_NCHITTEST: + if (lRet == IntPtr.Zero) + { + lRet = (IntPtr)HitTestNCA(hWnd, wParam, lParam); + + if (((HitTestValues)lRet) != HitTestValues.HTNOWHERE) + { + callDwp = false; + } + } + break; + } + + return lRet; + } + } + + public partial class WindowImpl + { + protected virtual unsafe IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + IntPtr lRet = IntPtr.Zero; + bool callDwp = true; + + if (DwmIsCompositionEnabled(out bool enabled) == 0) + { + lRet = CustomCaptionProc(hWnd, msg, wParam, lParam, ref callDwp); + } + + if (callDwp) + { + lRet = AppWndProc(hWnd, msg, wParam, lParam); + } + + return lRet; + } + } + public partial class WindowImpl { [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")] - protected virtual unsafe IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { const double wheelDelta = 120.0; uint timestamp = unchecked((uint)GetMessageTime()); From 9d05a144e1f0b299ac410b6328afa43b7a666ee5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 20 May 2020 15:12:32 -0300 Subject: [PATCH 05/67] fallback transparency should be null so that it doesnt hit test. --- src/Avalonia.Controls/TopLevel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 8335e03487..e56ff95e8e 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -402,7 +402,7 @@ namespace Avalonia.Controls } else { - _transparencyFallbackBorder.Background = Brushes.Transparent; + _transparencyFallbackBorder.Background = null; } } From fd73b308253a016791cf19947a7fdf3744a0a8c5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 20 May 2020 15:13:13 -0300 Subject: [PATCH 06/67] render hit test non-client area. --- .../Avalonia.Win32/WindowImpl.WndProc.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 82374893f1..001fa042c3 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -98,13 +98,14 @@ namespace Avalonia.Win32 LEFTEXTENDWIDTH = border_thickness.left; RIGHTEXTENDWIDTH = border_thickness.right; BOTTOMEXTENDWIDTH = border_thickness.bottom; + TOPEXTENDWIDTH = border_thickness.top; // Extend the frame into the client area. margins.cxLeftWidth = border_thickness.left; margins.cxRightWidth = border_thickness.right; margins.cyBottomHeight = border_thickness.bottom; margins.cyTopHeight = border_thickness.top; - + var hr = DwmExtendFrameIntoClientArea(hWnd, ref margins); //if (hr < 0) @@ -132,6 +133,21 @@ namespace Avalonia.Win32 { lRet = (IntPtr)HitTestNCA(hWnd, wParam, lParam); + uint timestamp = unchecked((uint)GetMessageTime()); + + if (((HitTestValues)lRet) == HitTestValues.HTCAPTION) + { + var position = PointToClient(PointFromLParam(lParam)); + + var visual = (_owner as Window).Renderer.HitTestFirst(position, _owner as Window, null); + + if(visual != null) + { + lRet = (IntPtr)HitTestValues.HTCLIENT; + } + + } + if (((HitTestValues)lRet) != HitTestValues.HTNOWHERE) { callDwp = false; From 458d45eac20effc5ed3273f93e3f5ab5e2c9bf2d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 20 May 2020 15:39:46 -0300 Subject: [PATCH 07/67] working win32 non-client area hit testing. --- src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 001fa042c3..6e825de23a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -138,8 +138,16 @@ namespace Avalonia.Win32 if (((HitTestValues)lRet) == HitTestValues.HTCAPTION) { var position = PointToClient(PointFromLParam(lParam)); - - var visual = (_owner as Window).Renderer.HitTestFirst(position, _owner as Window, null); + + var visual = (_owner as Window).Renderer.HitTestFirst(position, _owner as Window, x => + { + if(x is IInputElement ie && !ie.IsHitTestVisible) + { + return false; + } + + return true; + }); if(visual != null) { From f299c4492988f06b64429b52d646616e0808ee93 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 20 May 2020 16:54:20 -0300 Subject: [PATCH 08/67] add client area extending api to window. --- src/Avalonia.Controls/Platform/IWindowImpl.cs | 6 ++ src/Avalonia.Controls/Window.cs | 62 +++++++++++++++ .../Remote/PreviewerWindowImpl.cs | 6 ++ src/Avalonia.DesignerSupport/Remote/Stubs.cs | 6 ++ src/Avalonia.Native/WindowImpl.cs | 6 ++ src/Avalonia.X11/X11Window.cs | 6 ++ .../Avalonia.Win32/WindowImpl.WndProc.cs | 77 ++++++++++--------- 7 files changed, 134 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index cf31d30332..9c4808a41c 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -94,5 +94,11 @@ namespace Avalonia.Platform /// /// void SetMinMaxSize(Size minSize, Size maxSize); + + bool ExtendClientAreaToDecorationsHint { get; set; } + + Action ExtendClientAreaToDecorationsChanged { get; set; } + + Thickness ExtendedMargins { get; } } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 474d845905..55af5b69a3 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -71,6 +71,8 @@ namespace Avalonia.Controls { private List _children = new List(); + private bool _isExtendedIntoWindowDecorations; + /// /// Defines the property. /// @@ -87,6 +89,29 @@ namespace Avalonia.Controls o => o.HasSystemDecorations, (o, v) => o.HasSystemDecorations = v); + /// + /// Defines the property. + /// + public static readonly StyledProperty ExtendClientAreaToDecorationsHintProperty = + AvaloniaProperty.Register(nameof(ExtendClientAreaToDecorationsHint), false); + + + /// + /// Defines the property. + /// + public static readonly DirectProperty IsExtendedIntoWindowDecorationsProperty = + AvaloniaProperty.RegisterDirect(nameof(IsExtendedIntoWindowDecorations), + o => o.IsExtendedIntoWindowDecorations, + unsetValue: false); + + /// + /// Defines the property. + /// + public static readonly DirectProperty WindowDecorationMarginsProperty = + AvaloniaProperty.RegisterDirect(nameof(WindowDecorationMargins), + o => o.WindowDecorationMargins); + + /// /// Defines the property. /// @@ -164,6 +189,9 @@ namespace Avalonia.Controls WindowStateProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); + ExtendClientAreaToDecorationsHintProperty.Changed.AddClassHandler( + (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.ExtendClientAreaToDecorationsHint = (bool)e.NewValue; }); + MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); @@ -188,6 +216,7 @@ namespace Avalonia.Controls impl.Closing = HandleClosing; impl.GotInputWhenDisabled = OnGotInputWhenDisabled; impl.WindowStateChanged = HandleWindowStateChanged; + impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); @@ -237,6 +266,32 @@ namespace Avalonia.Controls } } + /// + /// Gets or sets if the ClientArea is Extended into the Window Decorations (chrome or border). + /// + public bool ExtendClientAreaToDecorationsHint + { + get { return GetValue(ExtendClientAreaToDecorationsHintProperty); } + set { SetValue(ExtendClientAreaToDecorationsHintProperty, value); } + } + + /// + /// Gets if the ClientArea is Extended into the Window Decorations. + /// + public bool IsExtendedIntoWindowDecorations + { + get => _isExtendedIntoWindowDecorations; + private set => SetAndRaise(IsExtendedIntoWindowDecorationsProperty, ref _isExtendedIntoWindowDecorations, value); + } + + private Thickness _windowDecorationMargins; + + public Thickness WindowDecorationMargins + { + get => _windowDecorationMargins; + private set => SetAndRaise(WindowDecorationMarginsProperty, ref _windowDecorationMargins, value); + } + /// /// Sets the system decorations (title bar, border, etc) /// @@ -438,6 +493,13 @@ namespace Avalonia.Controls } } + protected virtual void ExtendClientAreaToDecorationsChanged(bool isExtended) + { + IsExtendedIntoWindowDecorations = isExtended; + + WindowDecorationMargins = PlatformImpl.ExtendedMargins; + } + /// /// Hides the window but does not close it. /// diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 844489ef97..1da6da971b 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -84,6 +84,12 @@ namespace Avalonia.DesignerSupport.Remote public IScreenImpl Screen { get; } = new ScreenStub(); public Action GotInputWhenDisabled { get; set; } + + public bool ExtendClientAreaToDecorationsHint { get; set; } + + public Action ExtendClientAreaToDecorationsChanged { get; set; } + + public Thickness ExtendedMargins { get; } = new Thickness(); public void Activate() { diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 64b3af4ea2..f47bf58cdf 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -39,6 +39,12 @@ namespace Avalonia.DesignerSupport.Remote public Action TransparencyLevelChanged { get; set; } + public bool ExtendClientAreaToDecorationsHint { get; set; } + + public Action ExtendClientAreaToDecorationsChanged { get; set; } + + public Thickness ExtendedMargins { get; } = new Thickness(); + public WindowStub(IWindowImpl parent = null) { if (parent != null) diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index e91445000a..3495c4b392 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -98,6 +98,12 @@ namespace Avalonia.Native public Action WindowStateChanged { get; set; } + public bool ExtendClientAreaToDecorationsHint { get; set; } + + public Action ExtendClientAreaToDecorationsChanged { get; set; } + + public Thickness ExtendedMargins { get; } = new Thickness(); + public void ShowTaskbarIcon(bool value) { // NO OP On OSX diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 1d41fe4bdd..ef5a56fdc5 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -311,6 +311,12 @@ namespace Avalonia.X11 set => _transparencyHelper.TransparencyLevelChanged = value; } + public bool ExtendClientAreaToDecorationsHint { get; set; } + + public Action ExtendClientAreaToDecorationsChanged { get; set; } + + public Thickness ExtendedMargins { get; } = new Thickness(); + public Action Closed { get; set; } public Action PositionChanged { get; set; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 6e825de23a..44d35deaf0 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -14,11 +14,6 @@ namespace Avalonia.Win32 { public partial class WindowImpl { - private int LEFTEXTENDWIDTH = 4; - private int RIGHTEXTENDWIDTH = 4; - private int BOTTOMEXTENDWIDTH = 4; - private int TOPEXTENDWIDTH = 100; - // Hit test the frame for resizing and moving. HitTestValues HitTestNCA(IntPtr hWnd, IntPtr wParam, IntPtr lParam) { @@ -32,28 +27,40 @@ namespace Avalonia.Win32 RECT rcFrame = new RECT(); AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0); + RECT border_thickness = new RECT(); + if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME)) + { + AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0); + border_thickness.left *= -1; + border_thickness.top *= -1; + } + else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) + { + border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; + } + // Determine if the hit test is for resizing. Default middle (1,1). ushort uRow = 1; ushort uCol = 1; bool fOnResizeBorder = false; // Determine if the point is at the top or bottom of the window. - if (ptMouse.Y >= rcWindow.top && ptMouse.Y < rcWindow.top + TOPEXTENDWIDTH) + if (ptMouse.Y >= rcWindow.top && ptMouse.Y < rcWindow.top + border_thickness.top) { fOnResizeBorder = (ptMouse.Y < (rcWindow.top - rcFrame.top)); uRow = 0; } - else if (ptMouse.Y < rcWindow.bottom && ptMouse.Y >= rcWindow.bottom - BOTTOMEXTENDWIDTH) + else if (ptMouse.Y < rcWindow.bottom && ptMouse.Y >= rcWindow.bottom - border_thickness.bottom) { uRow = 2; } // Determine if the point is at the left or right of the window. - if (ptMouse.X >= rcWindow.left && ptMouse.X < rcWindow.left + LEFTEXTENDWIDTH) + if (ptMouse.X >= rcWindow.left && ptMouse.X < rcWindow.left + border_thickness.left) { uCol = 0; // left side } - else if (ptMouse.X < rcWindow.right && ptMouse.X >= rcWindow.right - RIGHTEXTENDWIDTH) + else if (ptMouse.X < rcWindow.right && ptMouse.X >= rcWindow.right - border_thickness.right) { uCol = 2; // right side } @@ -66,8 +73,6 @@ namespace Avalonia.Win32 new []{ HitTestValues.HTBOTTOMLEFT, HitTestValues.HTBOTTOM, HitTestValues.HTBOTTOMRIGHT }, }; - System.Diagnostics.Debug.WriteLine(hitTests[uRow][uCol]); - return hitTests[uRow][uCol]; } @@ -84,35 +89,37 @@ namespace Avalonia.Win32 { case WindowsMessage.WM_ACTIVATE: { - if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME)) - { - AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0); - border_thickness.left *= -1; - border_thickness.top *= -1; - } - else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) - { - border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; - } + if (!_isClientAreaExtended) + { + if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME)) + { + AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0); + border_thickness.left *= -1; + border_thickness.top *= -1; + } + else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) + { + border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; + } - LEFTEXTENDWIDTH = border_thickness.left; - RIGHTEXTENDWIDTH = border_thickness.right; - BOTTOMEXTENDWIDTH = border_thickness.bottom; - TOPEXTENDWIDTH = border_thickness.top; + // Extend the frame into the client area. + margins.cxLeftWidth = border_thickness.left; + margins.cxRightWidth = border_thickness.right; + margins.cyBottomHeight = border_thickness.bottom; + margins.cyTopHeight = border_thickness.top; - // Extend the frame into the client area. - margins.cxLeftWidth = border_thickness.left; - margins.cxRightWidth = border_thickness.right; - margins.cyBottomHeight = border_thickness.bottom; - margins.cyTopHeight = border_thickness.top; + var hr = DwmExtendFrameIntoClientArea(hWnd, ref margins); - var hr = DwmExtendFrameIntoClientArea(hWnd, ref margins); + //if (hr < 0) + { + // Handle the error. + } - //if (hr < 0) - { - // Handle the error. - } + _isClientAreaExtended = true; + _extendedMargins = new Thickness(margins.cxLeftWidth / Scaling, margins.cyTopHeight / Scaling, margins.cxRightWidth / Scaling, margins.cyBottomHeight / Scaling); + ExtendClientAreaToDecorationsChanged?.Invoke(true); + } lRet = IntPtr.Zero; callDwp = true; break; From 89a0f9ceec475b5709248d27b098f47e2b14d15f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 20 May 2020 16:54:42 -0300 Subject: [PATCH 09/67] Add win32 impl for client extending based on property value. --- .../Avalonia.Win32/WindowImpl.WndProc.cs | 54 ++++------------- src/Windows/Avalonia.Win32/WindowImpl.cs | 59 ++++++++++++++++++- 2 files changed, 70 insertions(+), 43 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 44d35deaf0..067d9c8068 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -80,50 +80,20 @@ namespace Avalonia.Win32 { IntPtr lRet = IntPtr.Zero; - callDwp = !DwmDefWindowProc(hWnd, msg, wParam, lParam, ref lRet); - - MARGINS margins = new MARGINS { cxLeftWidth = 0, cxRightWidth = 0, cyBottomHeight = 0, cyTopHeight = 100 }; - RECT border_thickness = new RECT(); - + callDwp = !DwmDefWindowProc(hWnd, msg, wParam, lParam, ref lRet); + switch ((WindowsMessage)msg) { - case WindowsMessage.WM_ACTIVATE: - { - if (!_isClientAreaExtended) - { - if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME)) - { - AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0); - border_thickness.left *= -1; - border_thickness.top *= -1; - } - else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) - { - border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; - } - - // Extend the frame into the client area. - margins.cxLeftWidth = border_thickness.left; - margins.cxRightWidth = border_thickness.right; - margins.cyBottomHeight = border_thickness.bottom; - margins.cyTopHeight = border_thickness.top; - - var hr = DwmExtendFrameIntoClientArea(hWnd, ref margins); - - //if (hr < 0) - { - // Handle the error. - } - - _isClientAreaExtended = true; - _extendedMargins = new Thickness(margins.cxLeftWidth / Scaling, margins.cyTopHeight / Scaling, margins.cxRightWidth / Scaling, margins.cyBottomHeight / Scaling); - ExtendClientAreaToDecorationsChanged?.Invoke(true); - - } - lRet = IntPtr.Zero; - callDwp = true; - break; - } + //case WindowsMessage.WM_ACTIVATE: + // { + // if (!_isClientAreaExtended) + // { + // ExtendClientArea(); + // } + // lRet = IntPtr.Zero; + // callDwp = true; + // break; + // } case WindowsMessage.WM_NCCALCSIZE: { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 0cf5a73b9f..cf708a9a47 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -40,6 +40,7 @@ namespace Avalonia.Win32 private SavedWindowInfo _savedWindowInfo; private bool _isFullScreenActive; + private bool _isClientAreaExtended; #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; @@ -67,6 +68,7 @@ namespace Avalonia.Win32 private Size _minSize; private Size _maxSize; private WindowImpl _parent; + private bool _extendClientAreaToDecorationsHint; public WindowImpl() { @@ -670,6 +672,43 @@ namespace Avalonia.Win32 TaskBarList.MarkFullscreen(_hwnd, fullscreen); } + private void ExtendClientArea () + { + if (!_isClientAreaExtended) + { + RECT border_thickness = new RECT(); + MARGINS margins = new MARGINS(); + + if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME)) + { + AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0); + border_thickness.left *= -1; + border_thickness.top *= -1; + } + else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) + { + border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; + } + + // Extend the frame into the client area. + margins.cxLeftWidth = border_thickness.left; + margins.cxRightWidth = border_thickness.right; + margins.cyBottomHeight = border_thickness.bottom; + margins.cyTopHeight = border_thickness.top; + + var hr = DwmExtendFrameIntoClientArea(_hwnd, ref margins); + + //if (hr < 0) + { + // Handle the error. + } + + _isClientAreaExtended = true; + _extendedMargins = new Thickness(margins.cxLeftWidth / Scaling, margins.cyTopHeight / Scaling, margins.cxRightWidth / Scaling, margins.cyBottomHeight / Scaling); + ExtendClientAreaToDecorationsChanged?.Invoke(true); + } + } + private void ShowWindow(WindowState state) { ShowWindowCommand command; @@ -910,7 +949,25 @@ namespace Avalonia.Win32 } } - IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + + public bool ExtendClientAreaToDecorationsHint + { + get => _extendClientAreaToDecorationsHint; + set + { + _extendClientAreaToDecorationsHint = true; + + ExtendClientArea(); + // TODO Trigger transition. + } + } + + public Action ExtendClientAreaToDecorationsChanged { get; set; } + + private Thickness _extendedMargins; + + public Thickness ExtendedMargins => _extendedMargins; private struct SavedWindowInfo { From ab20a80de62ebaa482fee4787d8c938c2c010857 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 20 May 2020 16:55:32 -0300 Subject: [PATCH 10/67] only report extended if we were able to. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index cf708a9a47..ac1192d527 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -698,14 +698,12 @@ namespace Avalonia.Win32 var hr = DwmExtendFrameIntoClientArea(_hwnd, ref margins); - //if (hr < 0) + if (hr == 0) { - // Handle the error. + _isClientAreaExtended = true; + _extendedMargins = new Thickness(margins.cxLeftWidth / Scaling, margins.cyTopHeight / Scaling, margins.cxRightWidth / Scaling, margins.cyBottomHeight / Scaling); + ExtendClientAreaToDecorationsChanged?.Invoke(true); } - - _isClientAreaExtended = true; - _extendedMargins = new Thickness(margins.cxLeftWidth / Scaling, margins.cyTopHeight / Scaling, margins.cxRightWidth / Scaling, margins.cyBottomHeight / Scaling); - ExtendClientAreaToDecorationsChanged?.Invoke(true); } } From 8592ceb55d56cafd88cc5921bfb29e2ff04760a7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 20 May 2020 17:06:00 -0300 Subject: [PATCH 11/67] check composition is allowed before allowing extended windows. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ac1192d527..2ee34496ba 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -676,6 +676,11 @@ namespace Avalonia.Win32 { if (!_isClientAreaExtended) { + if(DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled) + { + return; + } + RECT border_thickness = new RECT(); MARGINS margins = new MARGINS(); From ba2fa015c0f4c206784d6e3b25c7900da0ad4d86 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 20 May 2020 17:06:30 -0300 Subject: [PATCH 12/67] separate WndProcs --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 518 ++++++++++++++ .../WindowImpl.CustomCaptionProc.cs | 117 ++++ .../Avalonia.Win32/WindowImpl.WndProc.cs | 642 +----------------- 3 files changed, 636 insertions(+), 641 deletions(-) create mode 100644 src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs create mode 100644 src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs new file mode 100644 index 0000000000..b81634dce0 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -0,0 +1,518 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Win32.Input; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32 +{ + public partial class WindowImpl + { + [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", + Justification = "Using Win32 naming for consistency.")] + protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + const double wheelDelta = 120.0; + uint timestamp = unchecked((uint)GetMessageTime()); + + RawInputEventArgs e = null; + + switch ((WindowsMessage)msg) + { + case WindowsMessage.WM_ACTIVATE: + { + var wa = (WindowActivate)(ToInt32(wParam) & 0xffff); + + switch (wa) + { + case WindowActivate.WA_ACTIVE: + case WindowActivate.WA_CLICKACTIVE: + { + Activated?.Invoke(); + break; + } + + case WindowActivate.WA_INACTIVE: + { + Deactivated?.Invoke(); + break; + } + } + + return IntPtr.Zero; + } + + case WindowsMessage.WM_NCCALCSIZE: + { + if (ToInt32(wParam) == 1 && !HasFullDecorations || _isClientAreaExtended) + { + return IntPtr.Zero; + } + + break; + } + + case WindowsMessage.WM_CLOSE: + { + bool? preventClosing = Closing?.Invoke(); + if (preventClosing == true) + { + return IntPtr.Zero; + } + + break; + } + + case WindowsMessage.WM_DESTROY: + { + //Window doesn't exist anymore + _hwnd = IntPtr.Zero; + //Remove root reference to this class, so unmanaged delegate can be collected + s_instances.Remove(this); + Closed?.Invoke(); + + _mouseDevice.Dispose(); + _touchDevice?.Dispose(); + //Free other resources + Dispose(); + return IntPtr.Zero; + } + + case WindowsMessage.WM_DPICHANGED: + { + var dpi = ToInt32(wParam) & 0xffff; + var newDisplayRect = Marshal.PtrToStructure(lParam); + _scaling = dpi / 96.0; + ScalingChanged?.Invoke(_scaling); + SetWindowPos(hWnd, + IntPtr.Zero, + newDisplayRect.left, + newDisplayRect.top, + newDisplayRect.right - newDisplayRect.left, + newDisplayRect.bottom - newDisplayRect.top, + SetWindowPosFlags.SWP_NOZORDER | + SetWindowPosFlags.SWP_NOACTIVATE); + return IntPtr.Zero; + } + + case WindowsMessage.WM_KEYDOWN: + case WindowsMessage.WM_SYSKEYDOWN: + { + e = new RawKeyEventArgs( + WindowsKeyboardDevice.Instance, + timestamp, + _owner, + RawKeyEventType.KeyDown, + KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), + WindowsKeyboardDevice.Instance.Modifiers); + break; + } + + case WindowsMessage.WM_MENUCHAR: + { + // mute the system beep + return (IntPtr)((int)MenuCharParam.MNC_CLOSE << 16); + } + + case WindowsMessage.WM_KEYUP: + case WindowsMessage.WM_SYSKEYUP: + { + e = new RawKeyEventArgs( + WindowsKeyboardDevice.Instance, + timestamp, + _owner, + RawKeyEventType.KeyUp, + KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), + WindowsKeyboardDevice.Instance.Modifiers); + break; + } + case WindowsMessage.WM_CHAR: + { + // Ignore control chars + if (ToInt32(wParam) >= 32) + { + e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner, + new string((char)ToInt32(wParam), 1)); + } + + break; + } + + case WindowsMessage.WM_LBUTTONDOWN: + case WindowsMessage.WM_RBUTTONDOWN: + case WindowsMessage.WM_MBUTTONDOWN: + case WindowsMessage.WM_XBUTTONDOWN: + { + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + (WindowsMessage)msg switch + { + WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown, + WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown, + WindowsMessage.WM_MBUTTONDOWN => RawPointerEventType.MiddleButtonDown, + WindowsMessage.WM_XBUTTONDOWN => + HighWord(ToInt32(wParam)) == 1 ? + RawPointerEventType.XButton1Down : + RawPointerEventType.XButton2Down + }, + DipFromLParam(lParam), GetMouseModifiers(wParam)); + break; + } + + case WindowsMessage.WM_LBUTTONUP: + case WindowsMessage.WM_RBUTTONUP: + case WindowsMessage.WM_MBUTTONUP: + case WindowsMessage.WM_XBUTTONUP: + { + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + (WindowsMessage)msg switch + { + WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp, + WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp, + WindowsMessage.WM_MBUTTONUP => RawPointerEventType.MiddleButtonUp, + WindowsMessage.WM_XBUTTONUP => + HighWord(ToInt32(wParam)) == 1 ? + RawPointerEventType.XButton1Up : + RawPointerEventType.XButton2Up, + }, + DipFromLParam(lParam), GetMouseModifiers(wParam)); + break; + } + + case WindowsMessage.WM_MOUSEMOVE: + { + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + + if (!_trackingMouse) + { + var tm = new TRACKMOUSEEVENT + { + cbSize = Marshal.SizeOf(), + dwFlags = 2, + hwndTrack = _hwnd, + dwHoverTime = 0, + }; + + TrackMouseEvent(ref tm); + } + + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + RawPointerEventType.Move, + DipFromLParam(lParam), GetMouseModifiers(wParam)); + + break; + } + + case WindowsMessage.WM_MOUSEWHEEL: + { + e = new RawMouseWheelEventArgs( + _mouseDevice, + timestamp, + _owner, + PointToClient(PointFromLParam(lParam)), + new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam)); + break; + } + + case WindowsMessage.WM_MOUSEHWHEEL: + { + e = new RawMouseWheelEventArgs( + _mouseDevice, + timestamp, + _owner, + PointToClient(PointFromLParam(lParam)), + new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam)); + break; + } + + case WindowsMessage.WM_MOUSELEAVE: + { + _trackingMouse = false; + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + RawPointerEventType.LeaveWindow, + new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers); + break; + } + + case WindowsMessage.WM_NCLBUTTONDOWN: + case WindowsMessage.WM_NCRBUTTONDOWN: + case WindowsMessage.WM_NCMBUTTONDOWN: + case WindowsMessage.WM_NCXBUTTONDOWN: + { + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + (WindowsMessage)msg switch + { + WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType + .NonClientLeftButtonDown, + WindowsMessage.WM_NCRBUTTONDOWN => RawPointerEventType.RightButtonDown, + WindowsMessage.WM_NCMBUTTONDOWN => RawPointerEventType.MiddleButtonDown, + WindowsMessage.WM_NCXBUTTONDOWN => + HighWord(ToInt32(wParam)) == 1 ? + RawPointerEventType.XButton1Down : + RawPointerEventType.XButton2Down, + }, + PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam)); + break; + } + case WindowsMessage.WM_TOUCH: + { + var touchInputCount = wParam.ToInt32(); + + var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount]; + var touchInputs = new Span(pTouchInputs, touchInputCount); + + if (GetTouchInputInfo(lParam, (uint)touchInputCount, pTouchInputs, Marshal.SizeOf())) + { + foreach (var touchInput in touchInputs) + { + Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, + _owner, + touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ? + RawPointerEventType.TouchEnd : + touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ? + RawPointerEventType.TouchBegin : + RawPointerEventType.TouchUpdate, + PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)), + WindowsKeyboardDevice.Instance.Modifiers, + touchInput.Id)); + } + + CloseTouchInputHandle(lParam); + return IntPtr.Zero; + } + + break; + } + case WindowsMessage.WM_NCPAINT: + { + if (!HasFullDecorations) + { + return IntPtr.Zero; + } + + break; + } + + case WindowsMessage.WM_NCACTIVATE: + { + if (!HasFullDecorations) + { + return new IntPtr(1); + } + + break; + } + + case WindowsMessage.WM_PAINT: + { + using (_rendererLock.Lock()) + { + if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero) + { + var f = Scaling; + var r = ps.rcPaint; + Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, + (r.bottom - r.top) / f)); + EndPaint(_hwnd, ref ps); + } + } + + return IntPtr.Zero; + } + + case WindowsMessage.WM_SIZE: + { + using (_rendererLock.Lock()) + { + // Do nothing here, just block until the pending frame render is completed on the render thread + } + + var size = (SizeCommand)wParam; + + if (Resized != null && + (size == SizeCommand.Restored || + size == SizeCommand.Maximized)) + { + var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); + Resized(clientSize / Scaling); + } + + var windowState = size == SizeCommand.Maximized ? + WindowState.Maximized : + (size == SizeCommand.Minimized ? WindowState.Minimized : WindowState.Normal); + + if (windowState != _lastWindowState) + { + _lastWindowState = windowState; + WindowStateChanged?.Invoke(windowState); + } + + return IntPtr.Zero; + } + + case WindowsMessage.WM_MOVE: + { + PositionChanged?.Invoke(new PixelPoint((short)(ToInt32(lParam) & 0xffff), + (short)(ToInt32(lParam) >> 16))); + return IntPtr.Zero; + } + + case WindowsMessage.WM_GETMINMAXINFO: + { + MINMAXINFO mmi = Marshal.PtrToStructure(lParam); + + if (_minSize.Width > 0) + { + mmi.ptMinTrackSize.X = + (int)((_minSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); + } + + if (_minSize.Height > 0) + { + mmi.ptMinTrackSize.Y = + (int)((_minSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); + } + + if (!double.IsInfinity(_maxSize.Width) && _maxSize.Width > 0) + { + mmi.ptMaxTrackSize.X = + (int)((_maxSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); + } + + if (!double.IsInfinity(_maxSize.Height) && _maxSize.Height > 0) + { + mmi.ptMaxTrackSize.Y = + (int)((_maxSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); + } + + Marshal.StructureToPtr(mmi, lParam, true); + return IntPtr.Zero; + } + + case WindowsMessage.WM_DISPLAYCHANGE: + { + (Screen as ScreenImpl)?.InvalidateScreensCache(); + return IntPtr.Zero; + } + } + +#if USE_MANAGED_DRAG + if (_managedDrag.PreprocessInputEvent(ref e)) + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); +#endif + + if (e != null && Input != null) + { + Input(e); + + if (e.Handled) + { + return IntPtr.Zero; + } + } + + using (_rendererLock.Lock()) + { + return DefWindowProc(hWnd, msg, wParam, lParam); + } + } + + private static int ToInt32(IntPtr ptr) + { + if (IntPtr.Size == 4) + return ptr.ToInt32(); + + return (int)(ptr.ToInt64() & 0xffffffff); + } + + private static int HighWord(int param) => param >> 16; + + private Point DipFromLParam(IntPtr lParam) + { + return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling; + } + + private PixelPoint PointFromLParam(IntPtr lParam) + { + return new PixelPoint((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)); + } + + private bool ShouldIgnoreTouchEmulatedMessage() + { + if (!_multitouch) + { + return false; + } + + // MI_WP_SIGNATURE + // https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages + const long marker = 0xFF515700L; + + var info = GetMessageExtraInfo().ToInt64(); + return (info & marker) == marker; + } + + private static RawInputModifiers GetMouseModifiers(IntPtr wParam) + { + var keys = (ModifierKeys)ToInt32(wParam); + var modifiers = WindowsKeyboardDevice.Instance.Modifiers; + + if (keys.HasFlagCustom(ModifierKeys.MK_LBUTTON)) + { + modifiers |= RawInputModifiers.LeftMouseButton; + } + + if (keys.HasFlagCustom(ModifierKeys.MK_RBUTTON)) + { + modifiers |= RawInputModifiers.RightMouseButton; + } + + if (keys.HasFlagCustom(ModifierKeys.MK_MBUTTON)) + { + modifiers |= RawInputModifiers.MiddleMouseButton; + } + + if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON1)) + { + modifiers |= RawInputModifiers.XButton1MouseButton; + } + + if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON2)) + { + modifiers |= RawInputModifiers.XButton2MouseButton; + } + + return modifiers; + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs new file mode 100644 index 0000000000..faf83c6c9b --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Win32 +{ + public partial class WindowImpl + { + // Hit test the frame for resizing and moving. + HitTestValues HitTestNCA(IntPtr hWnd, IntPtr wParam, IntPtr lParam) + { + // Get the point coordinates for the hit test. + var ptMouse = PointFromLParam(lParam); + + // Get the window rectangle. + GetWindowRect(hWnd, out var rcWindow); + + // Get the frame rectangle, adjusted for the style without a caption. + RECT rcFrame = new RECT(); + AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0); + + RECT border_thickness = new RECT(); + if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME)) + { + AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0); + border_thickness.left *= -1; + border_thickness.top *= -1; + } + else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) + { + border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; + } + + // Determine if the hit test is for resizing. Default middle (1,1). + ushort uRow = 1; + ushort uCol = 1; + bool fOnResizeBorder = false; + + // Determine if the point is at the top or bottom of the window. + if (ptMouse.Y >= rcWindow.top && ptMouse.Y < rcWindow.top + border_thickness.top) + { + fOnResizeBorder = (ptMouse.Y < (rcWindow.top - rcFrame.top)); + uRow = 0; + } + else if (ptMouse.Y < rcWindow.bottom && ptMouse.Y >= rcWindow.bottom - border_thickness.bottom) + { + uRow = 2; + } + + // Determine if the point is at the left or right of the window. + if (ptMouse.X >= rcWindow.left && ptMouse.X < rcWindow.left + border_thickness.left) + { + uCol = 0; // left side + } + else if (ptMouse.X < rcWindow.right && ptMouse.X >= rcWindow.right - border_thickness.right) + { + uCol = 2; // right side + } + + // Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT) + HitTestValues[][] hitTests = new[] + { + new []{ HitTestValues.HTTOPLEFT, fOnResizeBorder ? HitTestValues.HTTOP : HitTestValues.HTCAPTION, HitTestValues.HTTOPRIGHT }, + new []{ HitTestValues.HTLEFT, HitTestValues.HTNOWHERE, HitTestValues.HTRIGHT }, + new []{ HitTestValues.HTBOTTOMLEFT, HitTestValues.HTBOTTOM, HitTestValues.HTBOTTOMRIGHT }, + }; + + return hitTests[uRow][uCol]; + } + + protected virtual IntPtr CustomCaptionProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool callDwp) + { + IntPtr lRet = IntPtr.Zero; + + callDwp = !DwmDefWindowProc(hWnd, msg, wParam, lParam, ref lRet); + + switch ((WindowsMessage)msg) + { + case WindowsMessage.WM_NCHITTEST: + if (lRet == IntPtr.Zero) + { + lRet = (IntPtr)HitTestNCA(hWnd, wParam, lParam); + + uint timestamp = unchecked((uint)GetMessageTime()); + + if (((HitTestValues)lRet) == HitTestValues.HTCAPTION) + { + var position = PointToClient(PointFromLParam(lParam)); + + var visual = (_owner as Window).Renderer.HitTestFirst(position, _owner as Window, x => + { + if (x is IInputElement ie && !ie.IsHitTestVisible) + { + return false; + } + + return true; + }); + + if (visual != null) + { + lRet = (IntPtr)HitTestValues.HTCLIENT; + } + } + + if (((HitTestValues)lRet) != HitTestValues.HTNOWHERE) + { + callDwp = false; + } + } + break; + } + + return lRet; + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 067d9c8068..bb6ab180cf 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -12,139 +12,6 @@ using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 { - public partial class WindowImpl - { - // Hit test the frame for resizing and moving. - HitTestValues HitTestNCA(IntPtr hWnd, IntPtr wParam, IntPtr lParam) - { - // Get the point coordinates for the hit test. - var ptMouse = PointFromLParam(lParam); - - // Get the window rectangle. - GetWindowRect(hWnd, out var rcWindow); - - // Get the frame rectangle, adjusted for the style without a caption. - RECT rcFrame = new RECT(); - AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0); - - RECT border_thickness = new RECT(); - if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME)) - { - AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0); - border_thickness.left *= -1; - border_thickness.top *= -1; - } - else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) - { - border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; - } - - // Determine if the hit test is for resizing. Default middle (1,1). - ushort uRow = 1; - ushort uCol = 1; - bool fOnResizeBorder = false; - - // Determine if the point is at the top or bottom of the window. - if (ptMouse.Y >= rcWindow.top && ptMouse.Y < rcWindow.top + border_thickness.top) - { - fOnResizeBorder = (ptMouse.Y < (rcWindow.top - rcFrame.top)); - uRow = 0; - } - else if (ptMouse.Y < rcWindow.bottom && ptMouse.Y >= rcWindow.bottom - border_thickness.bottom) - { - uRow = 2; - } - - // Determine if the point is at the left or right of the window. - if (ptMouse.X >= rcWindow.left && ptMouse.X < rcWindow.left + border_thickness.left) - { - uCol = 0; // left side - } - else if (ptMouse.X < rcWindow.right && ptMouse.X >= rcWindow.right - border_thickness.right) - { - uCol = 2; // right side - } - - // Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT) - HitTestValues[][] hitTests = new[] - { - new []{ HitTestValues.HTTOPLEFT, fOnResizeBorder ? HitTestValues.HTTOP : HitTestValues.HTCAPTION, HitTestValues.HTTOPRIGHT }, - new []{ HitTestValues.HTLEFT, HitTestValues.HTNOWHERE, HitTestValues.HTRIGHT }, - new []{ HitTestValues.HTBOTTOMLEFT, HitTestValues.HTBOTTOM, HitTestValues.HTBOTTOMRIGHT }, - }; - - return hitTests[uRow][uCol]; - } - - protected virtual unsafe IntPtr CustomCaptionProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool callDwp) - { - IntPtr lRet = IntPtr.Zero; - - callDwp = !DwmDefWindowProc(hWnd, msg, wParam, lParam, ref lRet); - - switch ((WindowsMessage)msg) - { - //case WindowsMessage.WM_ACTIVATE: - // { - // if (!_isClientAreaExtended) - // { - // ExtendClientArea(); - // } - // lRet = IntPtr.Zero; - // callDwp = true; - // break; - // } - - case WindowsMessage.WM_NCCALCSIZE: - { - if (ToInt32(wParam) == 1) - { - lRet = IntPtr.Zero; - callDwp = false; - } - break; - } - - case WindowsMessage.WM_NCHITTEST: - if (lRet == IntPtr.Zero) - { - lRet = (IntPtr)HitTestNCA(hWnd, wParam, lParam); - - uint timestamp = unchecked((uint)GetMessageTime()); - - if (((HitTestValues)lRet) == HitTestValues.HTCAPTION) - { - var position = PointToClient(PointFromLParam(lParam)); - - var visual = (_owner as Window).Renderer.HitTestFirst(position, _owner as Window, x => - { - if(x is IInputElement ie && !ie.IsHitTestVisible) - { - return false; - } - - return true; - }); - - if(visual != null) - { - lRet = (IntPtr)HitTestValues.HTCLIENT; - } - - } - - if (((HitTestValues)lRet) != HitTestValues.HTNOWHERE) - { - callDwp = false; - } - } - break; - } - - return lRet; - } - } - public partial class WindowImpl { protected virtual unsafe IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) @@ -152,7 +19,7 @@ namespace Avalonia.Win32 IntPtr lRet = IntPtr.Zero; bool callDwp = true; - if (DwmIsCompositionEnabled(out bool enabled) == 0) + if (_isClientAreaExtended) { lRet = CustomCaptionProc(hWnd, msg, wParam, lParam, ref callDwp); } @@ -165,511 +32,4 @@ namespace Avalonia.Win32 return lRet; } } - - public partial class WindowImpl - { - [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", - Justification = "Using Win32 naming for consistency.")] - protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - const double wheelDelta = 120.0; - uint timestamp = unchecked((uint)GetMessageTime()); - - RawInputEventArgs e = null; - - switch ((WindowsMessage)msg) - { - case WindowsMessage.WM_ACTIVATE: - { - var wa = (WindowActivate)(ToInt32(wParam) & 0xffff); - - switch (wa) - { - case WindowActivate.WA_ACTIVE: - case WindowActivate.WA_CLICKACTIVE: - { - Activated?.Invoke(); - break; - } - - case WindowActivate.WA_INACTIVE: - { - Deactivated?.Invoke(); - break; - } - } - - return IntPtr.Zero; - } - - case WindowsMessage.WM_NCCALCSIZE: - { - if (ToInt32(wParam) == 1 && !HasFullDecorations) - { - return IntPtr.Zero; - } - - break; - } - - case WindowsMessage.WM_CLOSE: - { - bool? preventClosing = Closing?.Invoke(); - if (preventClosing == true) - { - return IntPtr.Zero; - } - - break; - } - - case WindowsMessage.WM_DESTROY: - { - //Window doesn't exist anymore - _hwnd = IntPtr.Zero; - //Remove root reference to this class, so unmanaged delegate can be collected - s_instances.Remove(this); - Closed?.Invoke(); - - _mouseDevice.Dispose(); - _touchDevice?.Dispose(); - //Free other resources - Dispose(); - return IntPtr.Zero; - } - - case WindowsMessage.WM_DPICHANGED: - { - var dpi = ToInt32(wParam) & 0xffff; - var newDisplayRect = Marshal.PtrToStructure(lParam); - _scaling = dpi / 96.0; - ScalingChanged?.Invoke(_scaling); - SetWindowPos(hWnd, - IntPtr.Zero, - newDisplayRect.left, - newDisplayRect.top, - newDisplayRect.right - newDisplayRect.left, - newDisplayRect.bottom - newDisplayRect.top, - SetWindowPosFlags.SWP_NOZORDER | - SetWindowPosFlags.SWP_NOACTIVATE); - return IntPtr.Zero; - } - - case WindowsMessage.WM_KEYDOWN: - case WindowsMessage.WM_SYSKEYDOWN: - { - e = new RawKeyEventArgs( - WindowsKeyboardDevice.Instance, - timestamp, - _owner, - RawKeyEventType.KeyDown, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), - WindowsKeyboardDevice.Instance.Modifiers); - break; - } - - case WindowsMessage.WM_MENUCHAR: - { - // mute the system beep - return (IntPtr)((int)MenuCharParam.MNC_CLOSE << 16); - } - - case WindowsMessage.WM_KEYUP: - case WindowsMessage.WM_SYSKEYUP: - { - e = new RawKeyEventArgs( - WindowsKeyboardDevice.Instance, - timestamp, - _owner, - RawKeyEventType.KeyUp, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), - WindowsKeyboardDevice.Instance.Modifiers); - break; - } - case WindowsMessage.WM_CHAR: - { - // Ignore control chars - if (ToInt32(wParam) >= 32) - { - e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner, - new string((char)ToInt32(wParam), 1)); - } - - break; - } - - case WindowsMessage.WM_LBUTTONDOWN: - case WindowsMessage.WM_RBUTTONDOWN: - case WindowsMessage.WM_MBUTTONDOWN: - case WindowsMessage.WM_XBUTTONDOWN: - { - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } - - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - (WindowsMessage)msg switch - { - WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown, - WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown, - WindowsMessage.WM_MBUTTONDOWN => RawPointerEventType.MiddleButtonDown, - WindowsMessage.WM_XBUTTONDOWN => - HighWord(ToInt32(wParam)) == 1 ? - RawPointerEventType.XButton1Down : - RawPointerEventType.XButton2Down - }, - DipFromLParam(lParam), GetMouseModifiers(wParam)); - break; - } - - case WindowsMessage.WM_LBUTTONUP: - case WindowsMessage.WM_RBUTTONUP: - case WindowsMessage.WM_MBUTTONUP: - case WindowsMessage.WM_XBUTTONUP: - { - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } - - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - (WindowsMessage)msg switch - { - WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp, - WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp, - WindowsMessage.WM_MBUTTONUP => RawPointerEventType.MiddleButtonUp, - WindowsMessage.WM_XBUTTONUP => - HighWord(ToInt32(wParam)) == 1 ? - RawPointerEventType.XButton1Up : - RawPointerEventType.XButton2Up, - }, - DipFromLParam(lParam), GetMouseModifiers(wParam)); - break; - } - - case WindowsMessage.WM_MOUSEMOVE: - { - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } - - if (!_trackingMouse) - { - var tm = new TRACKMOUSEEVENT - { - cbSize = Marshal.SizeOf(), - dwFlags = 2, - hwndTrack = _hwnd, - dwHoverTime = 0, - }; - - TrackMouseEvent(ref tm); - } - - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - RawPointerEventType.Move, - DipFromLParam(lParam), GetMouseModifiers(wParam)); - - break; - } - - case WindowsMessage.WM_MOUSEWHEEL: - { - e = new RawMouseWheelEventArgs( - _mouseDevice, - timestamp, - _owner, - PointToClient(PointFromLParam(lParam)), - new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam)); - break; - } - - case WindowsMessage.WM_MOUSEHWHEEL: - { - e = new RawMouseWheelEventArgs( - _mouseDevice, - timestamp, - _owner, - PointToClient(PointFromLParam(lParam)), - new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam)); - break; - } - - case WindowsMessage.WM_MOUSELEAVE: - { - _trackingMouse = false; - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - RawPointerEventType.LeaveWindow, - new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers); - break; - } - - case WindowsMessage.WM_NCLBUTTONDOWN: - case WindowsMessage.WM_NCRBUTTONDOWN: - case WindowsMessage.WM_NCMBUTTONDOWN: - case WindowsMessage.WM_NCXBUTTONDOWN: - { - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - (WindowsMessage)msg switch - { - WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType - .NonClientLeftButtonDown, - WindowsMessage.WM_NCRBUTTONDOWN => RawPointerEventType.RightButtonDown, - WindowsMessage.WM_NCMBUTTONDOWN => RawPointerEventType.MiddleButtonDown, - WindowsMessage.WM_NCXBUTTONDOWN => - HighWord(ToInt32(wParam)) == 1 ? - RawPointerEventType.XButton1Down : - RawPointerEventType.XButton2Down, - }, - PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam)); - break; - } - case WindowsMessage.WM_TOUCH: - { - var touchInputCount = wParam.ToInt32(); - - var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount]; - var touchInputs = new Span(pTouchInputs, touchInputCount); - - if (GetTouchInputInfo(lParam, (uint)touchInputCount, pTouchInputs, Marshal.SizeOf())) - { - foreach (var touchInput in touchInputs) - { - Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, - _owner, - touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ? - RawPointerEventType.TouchEnd : - touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ? - RawPointerEventType.TouchBegin : - RawPointerEventType.TouchUpdate, - PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)), - WindowsKeyboardDevice.Instance.Modifiers, - touchInput.Id)); - } - - CloseTouchInputHandle(lParam); - return IntPtr.Zero; - } - - break; - } - case WindowsMessage.WM_NCPAINT: - { - if (!HasFullDecorations) - { - return IntPtr.Zero; - } - - break; - } - - case WindowsMessage.WM_NCACTIVATE: - { - if (!HasFullDecorations) - { - return new IntPtr(1); - } - - break; - } - - case WindowsMessage.WM_PAINT: - { - using (_rendererLock.Lock()) - { - if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero) - { - var f = Scaling; - var r = ps.rcPaint; - Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, - (r.bottom - r.top) / f)); - EndPaint(_hwnd, ref ps); - } - } - - return IntPtr.Zero; - } - - case WindowsMessage.WM_SIZE: - { - using (_rendererLock.Lock()) - { - // Do nothing here, just block until the pending frame render is completed on the render thread - } - - var size = (SizeCommand)wParam; - - if (Resized != null && - (size == SizeCommand.Restored || - size == SizeCommand.Maximized)) - { - var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); - Resized(clientSize / Scaling); - } - - var windowState = size == SizeCommand.Maximized ? - WindowState.Maximized : - (size == SizeCommand.Minimized ? WindowState.Minimized : WindowState.Normal); - - if (windowState != _lastWindowState) - { - _lastWindowState = windowState; - WindowStateChanged?.Invoke(windowState); - } - - return IntPtr.Zero; - } - - case WindowsMessage.WM_MOVE: - { - PositionChanged?.Invoke(new PixelPoint((short)(ToInt32(lParam) & 0xffff), - (short)(ToInt32(lParam) >> 16))); - return IntPtr.Zero; - } - - case WindowsMessage.WM_GETMINMAXINFO: - { - MINMAXINFO mmi = Marshal.PtrToStructure(lParam); - - if (_minSize.Width > 0) - { - mmi.ptMinTrackSize.X = - (int)((_minSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); - } - - if (_minSize.Height > 0) - { - mmi.ptMinTrackSize.Y = - (int)((_minSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); - } - - if (!double.IsInfinity(_maxSize.Width) && _maxSize.Width > 0) - { - mmi.ptMaxTrackSize.X = - (int)((_maxSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); - } - - if (!double.IsInfinity(_maxSize.Height) && _maxSize.Height > 0) - { - mmi.ptMaxTrackSize.Y = - (int)((_maxSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); - } - - Marshal.StructureToPtr(mmi, lParam, true); - return IntPtr.Zero; - } - - case WindowsMessage.WM_DISPLAYCHANGE: - { - (Screen as ScreenImpl)?.InvalidateScreensCache(); - return IntPtr.Zero; - } - } - -#if USE_MANAGED_DRAG - if (_managedDrag.PreprocessInputEvent(ref e)) - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); -#endif - - if (e != null && Input != null) - { - Input(e); - - if (e.Handled) - { - return IntPtr.Zero; - } - } - - using (_rendererLock.Lock()) - { - return DefWindowProc(hWnd, msg, wParam, lParam); - } - } - - private static int ToInt32(IntPtr ptr) - { - if (IntPtr.Size == 4) - return ptr.ToInt32(); - - return (int)(ptr.ToInt64() & 0xffffffff); - } - - private static int HighWord(int param) => param >> 16; - - private Point DipFromLParam(IntPtr lParam) - { - return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling; - } - - private PixelPoint PointFromLParam(IntPtr lParam) - { - return new PixelPoint((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)); - } - - private bool ShouldIgnoreTouchEmulatedMessage() - { - if (!_multitouch) - { - return false; - } - - // MI_WP_SIGNATURE - // https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages - const long marker = 0xFF515700L; - - var info = GetMessageExtraInfo().ToInt64(); - return (info & marker) == marker; - } - - private static RawInputModifiers GetMouseModifiers(IntPtr wParam) - { - var keys = (ModifierKeys)ToInt32(wParam); - var modifiers = WindowsKeyboardDevice.Instance.Modifiers; - - if (keys.HasFlagCustom(ModifierKeys.MK_LBUTTON)) - { - modifiers |= RawInputModifiers.LeftMouseButton; - } - - if (keys.HasFlagCustom(ModifierKeys.MK_RBUTTON)) - { - modifiers |= RawInputModifiers.RightMouseButton; - } - - if (keys.HasFlagCustom(ModifierKeys.MK_MBUTTON)) - { - modifiers |= RawInputModifiers.MiddleMouseButton; - } - - if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON1)) - { - modifiers |= RawInputModifiers.XButton1MouseButton; - } - - if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON2)) - { - modifiers |= RawInputModifiers.XButton2MouseButton; - } - - return modifiers; - } - } } From 8d8f76c5e8f902e5db6ee9b7ad0da2aa6ebe4c1c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 20 May 2020 17:08:28 -0300 Subject: [PATCH 13/67] TransparencyFallback layer needs to obey ExtendedClient margins to not cover titlebar. --- src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml | 2 +- src/Avalonia.Themes.Default/OverlayPopupHost.xaml | 2 +- src/Avalonia.Themes.Default/PopupRoot.xaml | 2 +- src/Avalonia.Themes.Default/Window.xaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml index 9ffe51fae8..f0e884832f 100644 --- a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml @@ -4,7 +4,7 @@ - + - + - + - + Date: Wed, 20 May 2020 17:24:27 -0300 Subject: [PATCH 14/67] fix embeddable control root. --- src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml | 2 +- src/Avalonia.Themes.Default/OverlayPopupHost.xaml | 2 +- src/Avalonia.Themes.Default/PopupRoot.xaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml index f0e884832f..9ffe51fae8 100644 --- a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml @@ -4,7 +4,7 @@ - + - + - + Date: Wed, 20 May 2020 17:29:07 -0300 Subject: [PATCH 15/67] fix build --- src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index faf83c6c9b..6716691872 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Text; +using Avalonia.Controls; +using Avalonia.Input; +using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 { From cb83a77623a6badc51b1e05191ea974e75fb9a2a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 20 May 2020 17:57:21 -0300 Subject: [PATCH 16/67] use thickness as the ExtendClientArea hint. --- src/Avalonia.Controls/Platform/IWindowImpl.cs | 2 +- src/Avalonia.Controls/Window.cs | 10 +++++----- .../Remote/PreviewerWindowImpl.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Native/WindowImpl.cs | 2 +- src/Avalonia.X11/X11Window.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 19 +++++++++---------- 7 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 9c4808a41c..604c3a61aa 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -95,7 +95,7 @@ namespace Avalonia.Platform /// void SetMinMaxSize(Size minSize, Size maxSize); - bool ExtendClientAreaToDecorationsHint { get; set; } + Thickness ExtendClientAreaToDecorationsHint { get; set; } Action ExtendClientAreaToDecorationsChanged { get; set; } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 55af5b69a3..eb8801622d 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -92,8 +92,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty ExtendClientAreaToDecorationsHintProperty = - AvaloniaProperty.Register(nameof(ExtendClientAreaToDecorationsHint), false); + public static readonly StyledProperty ExtendClientAreaToDecorationsHintProperty = + AvaloniaProperty.Register(nameof(ExtendClientAreaToDecorationsHint), default); /// @@ -109,7 +109,7 @@ namespace Avalonia.Controls /// public static readonly DirectProperty WindowDecorationMarginsProperty = AvaloniaProperty.RegisterDirect(nameof(WindowDecorationMargins), - o => o.WindowDecorationMargins); + o => o.WindowDecorationMargins); /// @@ -190,7 +190,7 @@ namespace Avalonia.Controls (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); ExtendClientAreaToDecorationsHintProperty.Changed.AddClassHandler( - (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.ExtendClientAreaToDecorationsHint = (bool)e.NewValue; }); + (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.ExtendClientAreaToDecorationsHint = (Thickness)e.NewValue; }); MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); @@ -269,7 +269,7 @@ namespace Avalonia.Controls /// /// Gets or sets if the ClientArea is Extended into the Window Decorations (chrome or border). /// - public bool ExtendClientAreaToDecorationsHint + public Thickness ExtendClientAreaToDecorationsHint { get { return GetValue(ExtendClientAreaToDecorationsHintProperty); } set { SetValue(ExtendClientAreaToDecorationsHintProperty, value); } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 1da6da971b..573a881c01 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -85,7 +85,7 @@ namespace Avalonia.DesignerSupport.Remote public IScreenImpl Screen { get; } = new ScreenStub(); public Action GotInputWhenDisabled { get; set; } - public bool ExtendClientAreaToDecorationsHint { get; set; } + public Thickness ExtendClientAreaToDecorationsHint { get; set; } public Action ExtendClientAreaToDecorationsChanged { get; set; } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index f47bf58cdf..805528c113 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -39,7 +39,7 @@ namespace Avalonia.DesignerSupport.Remote public Action TransparencyLevelChanged { get; set; } - public bool ExtendClientAreaToDecorationsHint { get; set; } + public Thickness ExtendClientAreaToDecorationsHint { get; set; } public Action ExtendClientAreaToDecorationsChanged { get; set; } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 3495c4b392..2e81a66d1f 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -98,7 +98,7 @@ namespace Avalonia.Native public Action WindowStateChanged { get; set; } - public bool ExtendClientAreaToDecorationsHint { get; set; } + public Thickness ExtendClientAreaToDecorationsHint { get; set; } public Action ExtendClientAreaToDecorationsChanged { get; set; } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index ef5a56fdc5..284c4e079e 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -311,7 +311,7 @@ namespace Avalonia.X11 set => _transparencyHelper.TransparencyLevelChanged = value; } - public bool ExtendClientAreaToDecorationsHint { get; set; } + public Thickness ExtendClientAreaToDecorationsHint { get; set; } public Action ExtendClientAreaToDecorationsChanged { get; set; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 2ee34496ba..78dcd91ff5 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -68,7 +68,7 @@ namespace Avalonia.Win32 private Size _minSize; private Size _maxSize; private WindowImpl _parent; - private bool _extendClientAreaToDecorationsHint; + private Thickness _extendClientAreaToDecorationsHint; public WindowImpl() { @@ -672,7 +672,7 @@ namespace Avalonia.Win32 TaskBarList.MarkFullscreen(_hwnd, fullscreen); } - private void ExtendClientArea () + private void ExtendClientArea (Thickness thickness) { if (!_isClientAreaExtended) { @@ -696,10 +696,10 @@ namespace Avalonia.Win32 } // Extend the frame into the client area. - margins.cxLeftWidth = border_thickness.left; - margins.cxRightWidth = border_thickness.right; - margins.cyBottomHeight = border_thickness.bottom; - margins.cyTopHeight = border_thickness.top; + margins.cxLeftWidth = thickness.Left == -1 ? border_thickness.left : (int)(thickness.Left * Scaling); + margins.cxRightWidth = thickness.Right == -1 ? border_thickness.right : (int)(thickness.Right * Scaling); + margins.cyBottomHeight = thickness.Bottom == -1 ? border_thickness.bottom : (int)(thickness.Bottom * Scaling); + margins.cyTopHeight = thickness.Top == -1 ? border_thickness.top : (int)(thickness.Top * Scaling); var hr = DwmExtendFrameIntoClientArea(_hwnd, ref margins); @@ -954,15 +954,14 @@ namespace Avalonia.Win32 IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; - public bool ExtendClientAreaToDecorationsHint + public Thickness ExtendClientAreaToDecorationsHint { get => _extendClientAreaToDecorationsHint; set { - _extendClientAreaToDecorationsHint = true; + _extendClientAreaToDecorationsHint = value; - ExtendClientArea(); - // TODO Trigger transition. + ExtendClientArea(value); } } From 8c461aed6f2b6d339a2c9b1d1771bf67d873a5d8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 09:33:17 -0300 Subject: [PATCH 17/67] add handler for DWM_COMPOSITION_CHANGED. --- src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index 6716691872..a55a22c3c3 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -79,6 +79,10 @@ namespace Avalonia.Win32 switch ((WindowsMessage)msg) { + case WindowsMessage.WM_DWMCOMPOSITIONCHANGED: + // TODO handle composition changed. + break; + case WindowsMessage.WM_NCHITTEST: if (lRet == IntPtr.Zero) { From 87e9454a83dfb314c2dd61a6fe57c262a4c946f4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 09:33:40 -0300 Subject: [PATCH 18/67] Revert "use thickness as the ExtendClientArea hint." This reverts commit 42673554085035e8a8a3435bc623ceb29f72e4db. --- src/Avalonia.Controls/Platform/IWindowImpl.cs | 2 +- src/Avalonia.Controls/Window.cs | 10 +++++----- .../Remote/PreviewerWindowImpl.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Native/WindowImpl.cs | 2 +- src/Avalonia.X11/X11Window.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 19 ++++++++++--------- 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 604c3a61aa..9c4808a41c 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -95,7 +95,7 @@ namespace Avalonia.Platform /// void SetMinMaxSize(Size minSize, Size maxSize); - Thickness ExtendClientAreaToDecorationsHint { get; set; } + bool ExtendClientAreaToDecorationsHint { get; set; } Action ExtendClientAreaToDecorationsChanged { get; set; } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index eb8801622d..55af5b69a3 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -92,8 +92,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty ExtendClientAreaToDecorationsHintProperty = - AvaloniaProperty.Register(nameof(ExtendClientAreaToDecorationsHint), default); + public static readonly StyledProperty ExtendClientAreaToDecorationsHintProperty = + AvaloniaProperty.Register(nameof(ExtendClientAreaToDecorationsHint), false); /// @@ -109,7 +109,7 @@ namespace Avalonia.Controls /// public static readonly DirectProperty WindowDecorationMarginsProperty = AvaloniaProperty.RegisterDirect(nameof(WindowDecorationMargins), - o => o.WindowDecorationMargins); + o => o.WindowDecorationMargins); /// @@ -190,7 +190,7 @@ namespace Avalonia.Controls (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); ExtendClientAreaToDecorationsHintProperty.Changed.AddClassHandler( - (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.ExtendClientAreaToDecorationsHint = (Thickness)e.NewValue; }); + (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.ExtendClientAreaToDecorationsHint = (bool)e.NewValue; }); MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); @@ -269,7 +269,7 @@ namespace Avalonia.Controls /// /// Gets or sets if the ClientArea is Extended into the Window Decorations (chrome or border). /// - public Thickness ExtendClientAreaToDecorationsHint + public bool ExtendClientAreaToDecorationsHint { get { return GetValue(ExtendClientAreaToDecorationsHintProperty); } set { SetValue(ExtendClientAreaToDecorationsHintProperty, value); } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 573a881c01..1da6da971b 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -85,7 +85,7 @@ namespace Avalonia.DesignerSupport.Remote public IScreenImpl Screen { get; } = new ScreenStub(); public Action GotInputWhenDisabled { get; set; } - public Thickness ExtendClientAreaToDecorationsHint { get; set; } + public bool ExtendClientAreaToDecorationsHint { get; set; } public Action ExtendClientAreaToDecorationsChanged { get; set; } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 805528c113..f47bf58cdf 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -39,7 +39,7 @@ namespace Avalonia.DesignerSupport.Remote public Action TransparencyLevelChanged { get; set; } - public Thickness ExtendClientAreaToDecorationsHint { get; set; } + public bool ExtendClientAreaToDecorationsHint { get; set; } public Action ExtendClientAreaToDecorationsChanged { get; set; } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 2e81a66d1f..3495c4b392 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -98,7 +98,7 @@ namespace Avalonia.Native public Action WindowStateChanged { get; set; } - public Thickness ExtendClientAreaToDecorationsHint { get; set; } + public bool ExtendClientAreaToDecorationsHint { get; set; } public Action ExtendClientAreaToDecorationsChanged { get; set; } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 284c4e079e..ef5a56fdc5 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -311,7 +311,7 @@ namespace Avalonia.X11 set => _transparencyHelper.TransparencyLevelChanged = value; } - public Thickness ExtendClientAreaToDecorationsHint { get; set; } + public bool ExtendClientAreaToDecorationsHint { get; set; } public Action ExtendClientAreaToDecorationsChanged { get; set; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 78dcd91ff5..2ee34496ba 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -68,7 +68,7 @@ namespace Avalonia.Win32 private Size _minSize; private Size _maxSize; private WindowImpl _parent; - private Thickness _extendClientAreaToDecorationsHint; + private bool _extendClientAreaToDecorationsHint; public WindowImpl() { @@ -672,7 +672,7 @@ namespace Avalonia.Win32 TaskBarList.MarkFullscreen(_hwnd, fullscreen); } - private void ExtendClientArea (Thickness thickness) + private void ExtendClientArea () { if (!_isClientAreaExtended) { @@ -696,10 +696,10 @@ namespace Avalonia.Win32 } // Extend the frame into the client area. - margins.cxLeftWidth = thickness.Left == -1 ? border_thickness.left : (int)(thickness.Left * Scaling); - margins.cxRightWidth = thickness.Right == -1 ? border_thickness.right : (int)(thickness.Right * Scaling); - margins.cyBottomHeight = thickness.Bottom == -1 ? border_thickness.bottom : (int)(thickness.Bottom * Scaling); - margins.cyTopHeight = thickness.Top == -1 ? border_thickness.top : (int)(thickness.Top * Scaling); + margins.cxLeftWidth = border_thickness.left; + margins.cxRightWidth = border_thickness.right; + margins.cyBottomHeight = border_thickness.bottom; + margins.cyTopHeight = border_thickness.top; var hr = DwmExtendFrameIntoClientArea(_hwnd, ref margins); @@ -954,14 +954,15 @@ namespace Avalonia.Win32 IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; - public Thickness ExtendClientAreaToDecorationsHint + public bool ExtendClientAreaToDecorationsHint { get => _extendClientAreaToDecorationsHint; set { - _extendClientAreaToDecorationsHint = value; + _extendClientAreaToDecorationsHint = true; - ExtendClientArea(value); + ExtendClientArea(); + // TODO Trigger transition. } } From c606d068dd6a6434f7d45fd1acc7e8fbf2b5b9a9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 11:19:20 -0300 Subject: [PATCH 19/67] add hints for further control over chrome. --- .../Platform/ExtendClientAreaChromeHints.cs | 17 ++++ src/Avalonia.Controls/Platform/IWindowImpl.cs | 6 +- src/Avalonia.Controls/Window.cs | 14 +++- .../Remote/PreviewerWindowImpl.cs | 14 +++- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 14 +++- src/Avalonia.Native/WindowImpl.cs | 14 +++- src/Avalonia.X11/X11Window.cs | 14 +++- .../Interop/UnmanagedMethods.cs | 6 ++ src/Windows/Avalonia.Win32/WindowImpl.cs | 77 +++++++++++++++---- 9 files changed, 145 insertions(+), 31 deletions(-) create mode 100644 src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs diff --git a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs new file mode 100644 index 0000000000..af33e3093b --- /dev/null +++ b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs @@ -0,0 +1,17 @@ +using System; + +namespace Avalonia.Platform +{ + [Flags] + public enum ExtendClientAreaChromeHints + { + Default = SystemTitleBar | SystemChromeButtons, + NoChrome, + SystemTitleBar = 0x01, + SystemChromeButtons = 0x02, + ManagedChromeButtons = 0x04, + PreferSystemChromeButtons = 0x08, + AdaptiveChromeWithTitleBar = SystemTitleBar | PreferSystemChromeButtons, + AdaptiveChromeWithoutTitleBar = PreferSystemChromeButtons, + } +} diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 9c4808a41c..d7bf93a603 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -95,7 +95,11 @@ namespace Avalonia.Platform /// void SetMinMaxSize(Size minSize, Size maxSize); - bool ExtendClientAreaToDecorationsHint { get; set; } + void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint); + + bool IsClientAreaExtendedToDecorations { get; } + + void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints); Action ExtendClientAreaToDecorationsChanged { get; set; } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 55af5b69a3..d37d363c63 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -95,6 +95,9 @@ namespace Avalonia.Controls public static readonly StyledProperty ExtendClientAreaToDecorationsHintProperty = AvaloniaProperty.Register(nameof(ExtendClientAreaToDecorationsHint), false); + public static readonly StyledProperty ExtendClientAreaChromeHintsProperty = + AvaloniaProperty.Register(nameof(ExtendClientAreaChromeHints), ExtendClientAreaChromeHints.Default); + /// /// Defines the property. @@ -190,7 +193,10 @@ namespace Avalonia.Controls (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); ExtendClientAreaToDecorationsHintProperty.Changed.AddClassHandler( - (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.ExtendClientAreaToDecorationsHint = (bool)e.NewValue; }); + (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.SetExtendClientAreaToDecorationsHint((bool)e.NewValue); }); + + ExtendClientAreaChromeHintsProperty.Changed.AddClassHandler( + (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.SetExtendClientAreaChromeHints((ExtendClientAreaChromeHints)e.NewValue); }); MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); @@ -275,6 +281,12 @@ namespace Avalonia.Controls set { SetValue(ExtendClientAreaToDecorationsHintProperty, value); } } + public ExtendClientAreaChromeHints ExtendClientAreaChromeHints + { + get => GetValue(ExtendClientAreaChromeHintsProperty); + set => SetValue(ExtendClientAreaChromeHintsProperty, value); + } + /// /// Gets if the ClientArea is Extended into the Window Decorations. /// diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 1da6da971b..05e355d460 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -83,14 +83,14 @@ namespace Avalonia.DesignerSupport.Remote } public IScreenImpl Screen { get; } = new ScreenStub(); - public Action GotInputWhenDisabled { get; set; } - - public bool ExtendClientAreaToDecorationsHint { get; set; } + public Action GotInputWhenDisabled { get; set; } public Action ExtendClientAreaToDecorationsChanged { get; set; } public Thickness ExtendedMargins { get; } = new Thickness(); + public bool IsClientAreaExtendedToDecorations { get; } + public void Activate() { } @@ -130,5 +130,13 @@ namespace Avalonia.DesignerSupport.Remote public void SetEnabled(bool enable) { } + + public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) + { + } + + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) + { + } } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index f47bf58cdf..104884c670 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -37,9 +37,7 @@ namespace Avalonia.DesignerSupport.Remote public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } - public Action TransparencyLevelChanged { get; set; } - - public bool ExtendClientAreaToDecorationsHint { get; set; } + public Action TransparencyLevelChanged { get; set; } public Action ExtendClientAreaToDecorationsChanged { get; set; } @@ -146,6 +144,14 @@ namespace Avalonia.DesignerSupport.Remote { } + public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) + { + } + + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) + { + } + public IPopupPositioner PopupPositioner { get; } public Action GotInputWhenDisabled { get; set; } @@ -157,6 +163,8 @@ namespace Avalonia.DesignerSupport.Remote } public WindowTransparencyLevel TransparencyLevel { get; private set; } + + public bool IsClientAreaExtendedToDecorations { get; } } class ClipboardStub : IClipboard diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 3495c4b392..47c8291803 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -96,14 +96,22 @@ namespace Avalonia.Native } } - public Action WindowStateChanged { get; set; } - - public bool ExtendClientAreaToDecorationsHint { get; set; } + public Action WindowStateChanged { get; set; } public Action ExtendClientAreaToDecorationsChanged { get; set; } public Thickness ExtendedMargins { get; } = new Thickness(); + public bool IsClientAreaExtendedToDecorations { get; } + + public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) + { + } + + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) + { + } + public void ShowTaskbarIcon(bool value) { // NO OP On OSX diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index ef5a56fdc5..2079d7df16 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -309,14 +309,14 @@ namespace Avalonia.X11 { get => _transparencyHelper.TransparencyLevelChanged; set => _transparencyHelper.TransparencyLevelChanged = value; - } - - public bool ExtendClientAreaToDecorationsHint { get; set; } + } public Action ExtendClientAreaToDecorationsChanged { get; set; } public Thickness ExtendedMargins { get; } = new Thickness(); + public bool IsClientAreaExtendedToDecorations { get; } + public Action Closed { get; set; } public Action PositionChanged { get; set; } @@ -1041,6 +1041,14 @@ namespace Avalonia.X11 _disabled = !enable; } + public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) + { + } + + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) + { + } + public Action GotInputWhenDisabled { get; set; } public void SetIcon(IWindowIconImpl icon) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index b66de8e0a8..6f843324f2 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -989,6 +989,12 @@ namespace Avalonia.Win32.Interop } } + [DllImport("user32.dll")] + public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert); + + [DllImport("user32.dll")] + public static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable); + [DllImport("user32.dll", SetLastError = true)] public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 2ee34496ba..c01adeb76e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -41,6 +41,7 @@ namespace Avalonia.Win32 private SavedWindowInfo _savedWindowInfo; private bool _isFullScreenActive; private bool _isClientAreaExtended; + private Thickness _extendedMargins; #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; @@ -67,8 +68,7 @@ namespace Avalonia.Win32 private OleDropTarget _dropTarget; private Size _minSize; private Size _maxSize; - private WindowImpl _parent; - private bool _extendClientAreaToDecorationsHint; + private WindowImpl _parent; public WindowImpl() { @@ -672,7 +672,7 @@ namespace Avalonia.Win32 TaskBarList.MarkFullscreen(_hwnd, fullscreen); } - private void ExtendClientArea () + private void ExtendClientArea (ExtendClientAreaChromeHints hints) { if (!_isClientAreaExtended) { @@ -689,6 +689,16 @@ namespace Avalonia.Win32 AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0); border_thickness.left *= -1; border_thickness.top *= -1; + + border_thickness.left = 1; + border_thickness.bottom = 1; + border_thickness.right = 1; + + if (!hints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar)) + { + border_thickness.top = 1; + } + } else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) { @@ -703,6 +713,19 @@ namespace Avalonia.Win32 var hr = DwmExtendFrameIntoClientArea(_hwnd, ref margins); + if(!hints.HasFlag(ExtendClientAreaChromeHints.SystemChromeButtons) || + (hints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChromeButtons) && + !hints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar))) + { + var style = GetStyle(); + + style &= ~(WindowStyles.WS_MINIMIZEBOX | WindowStyles.WS_MAXIMIZEBOX | WindowStyles.WS_SYSMENU); + + SetStyle(style); + + DisableCloseButton(_hwnd); + } + if (hr == 0) { _isClientAreaExtended = true; @@ -860,9 +883,10 @@ namespace Avalonia.Win32 // Otherwise it will still show in the taskbar. } + WindowStyles style; if ((oldProperties.IsResizable != newProperties.IsResizable) || forceChanges) { - var style = GetStyle(); + style = GetStyle(); if (newProperties.IsResizable) { @@ -883,7 +907,7 @@ namespace Avalonia.Win32 if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges) { - var style = GetStyle(); + style = GetStyle(); const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU; @@ -928,7 +952,26 @@ namespace Avalonia.Win32 SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); } - } + } + } + + private const int MF_BYCOMMAND = 0x0; + private const int MF_BYPOSITION = 0x400; + private const int MF_REMOVE = 0x1000; + private const int MF_ENABLED = 0x0; + private const int MF_GRAYED = 0x1; + private const int MF_DISABLED = 0x2; + private const int SC_CLOSE = 0xF060; + + void DisableCloseButton(IntPtr hwnd) + { + EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE, + MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); + } + void EnableCloseButton(IntPtr hwnd) + { + EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE, + MF_BYCOMMAND | MF_ENABLED); } #if USE_MANAGED_DRAG @@ -952,23 +995,23 @@ namespace Avalonia.Win32 } } - IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; - public bool ExtendClientAreaToDecorationsHint + public void SetExtendClientAreaToDecorationsHint(bool hint) { - get => _extendClientAreaToDecorationsHint; - set - { - _extendClientAreaToDecorationsHint = true; + ExtendClientArea(_extendChromeHints); + } - ExtendClientArea(); - // TODO Trigger transition. - } + private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; + + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) + { + _extendChromeHints = hints; } - public Action ExtendClientAreaToDecorationsChanged { get; set; } + public bool IsClientAreaExtendedToDecorations => _isClientAreaExtended; - private Thickness _extendedMargins; + public Action ExtendClientAreaToDecorationsChanged { get; set; } public Thickness ExtendedMargins => _extendedMargins; From a955e6fe9315b7d24b2ae40e47442a163ab1fa10 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 13:35:50 -0300 Subject: [PATCH 20/67] Add a readonly offscreen margin, to get the portion of the window that is offscreen. --- src/Avalonia.Controls/Platform/IWindowImpl.cs | 2 ++ src/Avalonia.Controls/Window.cs | 16 ++++++++- .../Remote/PreviewerWindowImpl.cs | 2 ++ src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 ++ src/Avalonia.Native/WindowImpl.cs | 2 ++ src/Avalonia.X11/X11Window.cs | 2 ++ .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 5 +++ src/Windows/Avalonia.Win32/WindowImpl.cs | 36 ++++++++++++++++++- 8 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index d7bf93a603..999f4f4201 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -104,5 +104,7 @@ namespace Avalonia.Platform Action ExtendClientAreaToDecorationsChanged { get; set; } Thickness ExtendedMargins { get; } + + Thickness OffScreenMargin { get; } } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index d37d363c63..0e26ce204c 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -113,7 +113,11 @@ namespace Avalonia.Controls public static readonly DirectProperty WindowDecorationMarginsProperty = AvaloniaProperty.RegisterDirect(nameof(WindowDecorationMargins), o => o.WindowDecorationMargins); - + + public static readonly DirectProperty OffScreenMarginProperty = + AvaloniaProperty.RegisterDirect(nameof(OffScreenMargin), + o => o.OffScreenMargin); + /// /// Defines the property. @@ -304,6 +308,14 @@ namespace Avalonia.Controls private set => SetAndRaise(WindowDecorationMarginsProperty, ref _windowDecorationMargins, value); } + private Thickness _offScreenMargin; + + public Thickness OffScreenMargin + { + get => _offScreenMargin; + private set => SetAndRaise(OffScreenMarginProperty, ref _offScreenMargin, value); + } + /// /// Sets the system decorations (title bar, border, etc) /// @@ -510,6 +522,8 @@ namespace Avalonia.Controls IsExtendedIntoWindowDecorations = isExtended; WindowDecorationMargins = PlatformImpl.ExtendedMargins; + + OffScreenMargin = PlatformImpl.OffScreenMargin; } /// diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 05e355d460..2d38f1dd61 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -91,6 +91,8 @@ namespace Avalonia.DesignerSupport.Remote public bool IsClientAreaExtendedToDecorations { get; } + public Thickness OffScreenMargin { get; } = new Thickness(); + public void Activate() { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 104884c670..ea53ce6f04 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -43,6 +43,8 @@ namespace Avalonia.DesignerSupport.Remote public Thickness ExtendedMargins { get; } = new Thickness(); + public Thickness OffScreenMargin { get; } = new Thickness(); + public WindowStub(IWindowImpl parent = null) { if (parent != null) diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 47c8291803..cbea340246 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -102,6 +102,8 @@ namespace Avalonia.Native public Thickness ExtendedMargins { get; } = new Thickness(); + public Thickness OffScreenMargin { get; } = new Thickness(); + public bool IsClientAreaExtendedToDecorations { get; } public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 2079d7df16..d9d05b51d6 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -315,6 +315,8 @@ namespace Avalonia.X11 public Thickness ExtendedMargins { get; } = new Thickness(); + public Thickness OffScreenMargin { get; } = new Thickness(); + public bool IsClientAreaExtendedToDecorations { get; } public Action Closed { get; set; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index b81634dce0..0c2160de2b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -374,6 +374,11 @@ namespace Avalonia.Win32 if (windowState != _lastWindowState) { _lastWindowState = windowState; + + UpdateExtendMargins(); + + ExtendClientAreaToDecorationsChanged?.Invoke(true); + WindowStateChanged?.Invoke(windowState); } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index c01adeb76e..9891615a85 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -42,7 +42,8 @@ namespace Avalonia.Win32 private bool _isFullScreenActive; private bool _isClientAreaExtended; private Thickness _extendedMargins; - + private Thickness _offScreenMargin; + #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; #endif @@ -672,6 +673,37 @@ namespace Avalonia.Win32 TaskBarList.MarkFullscreen(_hwnd, fullscreen); } + private MARGINS UpdateExtendMargins() + { + RECT borderThickness = new RECT(); + RECT borderCaptionThickness = new RECT(); + + AdjustWindowRectEx(ref borderCaptionThickness, (uint)(GetStyle()), false, 0); + AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle() & ~WindowStyles.WS_CAPTION), false, 0); + borderThickness.left *= -1; + borderThickness.top *= -1; + borderCaptionThickness.left *= -1; + borderCaptionThickness.top *= -1; + + _extendedMargins = new Thickness(1 / Scaling, borderCaptionThickness.top / Scaling, 1 / Scaling, 1 / Scaling); + + if (WindowState == WindowState.Maximized) + { + _offScreenMargin = new Thickness(borderThickness.left / Scaling, borderThickness.top / Scaling, borderThickness.right / Scaling, borderThickness.bottom / Scaling); + } + else + { + _offScreenMargin = new Thickness(); + } + + MARGINS margins = new MARGINS(); + margins.cxLeftWidth = 1; + margins.cxRightWidth = 1; + margins.cyBottomHeight = 1; + margins.cyTopHeight = borderThickness.top; + return margins; + } + private void ExtendClientArea (ExtendClientAreaChromeHints hints) { if (!_isClientAreaExtended) @@ -1015,6 +1047,8 @@ namespace Avalonia.Win32 public Thickness ExtendedMargins => _extendedMargins; + public Thickness OffScreenMargin => _offScreenMargin; + private struct SavedWindowInfo { public WindowStyles Style { get; set; } From d50f1ca6d34154ce2321edfbbe49a0af03d0952d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 13:41:57 -0300 Subject: [PATCH 21/67] calculate extend metrics in function. --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 39 ++++--------------- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 0c2160de2b..5ffed48f2d 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -375,7 +375,7 @@ namespace Avalonia.Win32 { _lastWindowState = windowState; - UpdateExtendMargins(); + UpdateExtendMargins(_extendChromeHints); ExtendClientAreaToDecorationsChanged?.Invoke(true); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9891615a85..fd54365d7b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -673,7 +673,7 @@ namespace Avalonia.Win32 TaskBarList.MarkFullscreen(_hwnd, fullscreen); } - private MARGINS UpdateExtendMargins() + private MARGINS UpdateExtendMargins(ExtendClientAreaChromeHints hints) { RECT borderThickness = new RECT(); RECT borderCaptionThickness = new RECT(); @@ -685,6 +685,11 @@ namespace Avalonia.Win32 borderCaptionThickness.left *= -1; borderCaptionThickness.top *= -1; + if (!hints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar)) + { + borderCaptionThickness.top = 1; + } + _extendedMargins = new Thickness(1 / Scaling, borderCaptionThickness.top / Scaling, 1 / Scaling, 1 / Scaling); if (WindowState == WindowState.Maximized) @@ -700,7 +705,7 @@ namespace Avalonia.Win32 margins.cxLeftWidth = 1; margins.cxRightWidth = 1; margins.cyBottomHeight = 1; - margins.cyTopHeight = borderThickness.top; + margins.cyTopHeight = borderCaptionThickness.top; return margins; } @@ -713,35 +718,7 @@ namespace Avalonia.Win32 return; } - RECT border_thickness = new RECT(); - MARGINS margins = new MARGINS(); - - if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME)) - { - AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0); - border_thickness.left *= -1; - border_thickness.top *= -1; - - border_thickness.left = 1; - border_thickness.bottom = 1; - border_thickness.right = 1; - - if (!hints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar)) - { - border_thickness.top = 1; - } - - } - else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) - { - border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; - } - - // Extend the frame into the client area. - margins.cxLeftWidth = border_thickness.left; - margins.cxRightWidth = border_thickness.right; - margins.cyBottomHeight = border_thickness.bottom; - margins.cyTopHeight = border_thickness.top; + var margins = UpdateExtendMargins(hints); var hr = DwmExtendFrameIntoClientArea(_hwnd, ref margins); From f91d07c14317c4d7dd9c98c08c8cc5076cc5afc7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 15:05:28 -0300 Subject: [PATCH 22/67] fix hit testing. --- .../WindowImpl.CustomCaptionProc.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index a55a22c3c3..fee6bb90bf 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; -using System.Text; +using System.Diagnostics; using Avalonia.Controls; using Avalonia.Input; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -34,6 +33,11 @@ namespace Avalonia.Win32 border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; } + if (_extendTitleBarHint >= 0) + { + border_thickness.top = (int)(_extendedMargins.Top * Scaling); + } + // Determine if the hit test is for resizing. Default middle (1,1). ushort uRow = 1; ushort uCol = 1; @@ -86,11 +90,13 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCHITTEST: if (lRet == IntPtr.Zero) { - lRet = (IntPtr)HitTestNCA(hWnd, wParam, lParam); + var hittestResult = HitTestNCA(hWnd, wParam, lParam); + + lRet = (IntPtr)hittestResult; uint timestamp = unchecked((uint)GetMessageTime()); - if (((HitTestValues)lRet) == HitTestValues.HTCAPTION) + if (hittestResult == HitTestValues.HTCAPTION) { var position = PointToClient(PointFromLParam(lParam)); @@ -106,11 +112,12 @@ namespace Avalonia.Win32 if (visual != null) { - lRet = (IntPtr)HitTestValues.HTCLIENT; + hittestResult = HitTestValues.HTCLIENT; + lRet = (IntPtr)hittestResult; } } - if (((HitTestValues)lRet) != HitTestValues.HTNOWHERE) + if (hittestResult != HitTestValues.HTNOWHERE) { callDwp = false; } From e070179b2d4fd26e4b6685dd79ce2f246d5da441 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 15:08:03 -0300 Subject: [PATCH 23/67] Configurable titlebar height. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 32 +++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index fd54365d7b..4b51a9dc42 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -685,12 +685,26 @@ namespace Avalonia.Win32 borderCaptionThickness.left *= -1; borderCaptionThickness.top *= -1; - if (!hints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar)) + bool wantsTitleBar = hints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar) || _extendTitleBarHint == -1; + + if (!wantsTitleBar) { borderCaptionThickness.top = 1; } - _extendedMargins = new Thickness(1 / Scaling, borderCaptionThickness.top / Scaling, 1 / Scaling, 1 / Scaling); + MARGINS margins = new MARGINS(); + margins.cxLeftWidth = 1; + margins.cxRightWidth = 1; + margins.cyBottomHeight = 1; + + if (_extendTitleBarHint != -1) + { + borderCaptionThickness.top = (int)(_extendTitleBarHint * Scaling); + } + + margins.cyTopHeight = hints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar) ? borderCaptionThickness.top : 1; + + _extendedMargins = new Thickness(0, borderCaptionThickness.top / Scaling, 0, 0); if (WindowState == WindowState.Maximized) { @@ -701,11 +715,6 @@ namespace Avalonia.Win32 _offScreenMargin = new Thickness(); } - MARGINS margins = new MARGINS(); - margins.cxLeftWidth = 1; - margins.cxRightWidth = 1; - margins.cyBottomHeight = 1; - margins.cyTopHeight = borderCaptionThickness.top; return margins; } @@ -737,8 +746,7 @@ namespace Avalonia.Win32 if (hr == 0) { - _isClientAreaExtended = true; - _extendedMargins = new Thickness(margins.cxLeftWidth / Scaling, margins.cyTopHeight / Scaling, margins.cxRightWidth / Scaling, margins.cyBottomHeight / Scaling); + _isClientAreaExtended = true; ExtendClientAreaToDecorationsChanged?.Invoke(true); } } @@ -1018,6 +1026,12 @@ namespace Avalonia.Win32 _extendChromeHints = hints; } + private double _extendTitleBarHint = -1; + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) + { + _extendTitleBarHint = titleBarHeight; + } + public bool IsClientAreaExtendedToDecorations => _isClientAreaExtended; public Action ExtendClientAreaToDecorationsChanged { get; set; } From 0cb4291ec0c0b5109a4354595eaffdbb1d6238d9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 15:08:33 -0300 Subject: [PATCH 24/67] Add TitleBarHeight Hint. --- src/Avalonia.Controls/Platform/IWindowImpl.cs | 2 ++ src/Avalonia.Controls/Window.cs | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 999f4f4201..15315063fe 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -101,6 +101,8 @@ namespace Avalonia.Platform void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints); + void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight); + Action ExtendClientAreaToDecorationsChanged { get; set; } Thickness ExtendedMargins { get; } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 0e26ce204c..9e7b9ea288 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -98,6 +98,9 @@ namespace Avalonia.Controls public static readonly StyledProperty ExtendClientAreaChromeHintsProperty = AvaloniaProperty.Register(nameof(ExtendClientAreaChromeHints), ExtendClientAreaChromeHints.Default); + public static readonly StyledProperty ExtendClientAreaTitleBarHeightHintProperty = + AvaloniaProperty.Register(nameof(ExtendClientAreaTitleBarHeightHint), -1); + /// /// Defines the property. @@ -202,6 +205,9 @@ namespace Avalonia.Controls ExtendClientAreaChromeHintsProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.SetExtendClientAreaChromeHints((ExtendClientAreaChromeHints)e.NewValue); }); + ExtendClientAreaTitleBarHeightHintProperty.Changed.AddClassHandler( + (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.SetExtendClientAreaTitleBarHeightHint((double)e.NewValue); }); + MinWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight))); MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); @@ -291,6 +297,12 @@ namespace Avalonia.Controls set => SetValue(ExtendClientAreaChromeHintsProperty, value); } + public double ExtendClientAreaTitleBarHeightHint + { + get => GetValue(ExtendClientAreaTitleBarHeightHintProperty); + set => SetValue(ExtendClientAreaTitleBarHeightHintProperty, value); + } + /// /// Gets if the ClientArea is Extended into the Window Decorations. /// From 3f3ce51a1d49135174043be3a5152093b5d5c349 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 15:08:48 -0300 Subject: [PATCH 25/67] Stubs for TitleBarHeight Hint. --- src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs | 4 ++++ src/Avalonia.DesignerSupport/Remote/Stubs.cs | 4 ++++ src/Avalonia.Native/WindowImpl.cs | 4 ++++ src/Avalonia.X11/X11Window.cs | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 2d38f1dd61..beea99c2ac 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -140,5 +140,9 @@ namespace Avalonia.DesignerSupport.Remote public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) { } + + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) + { + } } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index ea53ce6f04..1c02b354be 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -154,6 +154,10 @@ namespace Avalonia.DesignerSupport.Remote { } + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) + { + } + public IPopupPositioner PopupPositioner { get; } public Action GotInputWhenDisabled { get; set; } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index cbea340246..532869e751 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -114,6 +114,10 @@ namespace Avalonia.Native { } + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) + { + } + public void ShowTaskbarIcon(bool value) { // NO OP On OSX diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index d9d05b51d6..3760de88ed 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -1051,6 +1051,10 @@ namespace Avalonia.X11 { } + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) + { + } + public Action GotInputWhenDisabled { get; set; } public void SetIcon(IWindowIconImpl icon) From c233347958a9bab703723ddfc1805e455d5795dd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 15:42:23 -0300 Subject: [PATCH 26/67] Allow extend hints to be changed at runtime. --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 11 ++-- src/Windows/Avalonia.Win32/WindowImpl.cs | 60 ++++++++++++------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 5ffed48f2d..18283432e4 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -373,13 +373,16 @@ namespace Avalonia.Win32 if (windowState != _lastWindowState) { - _lastWindowState = windowState; + _lastWindowState = windowState; - UpdateExtendMargins(_extendChromeHints); + WindowStateChanged?.Invoke(windowState); - ExtendClientAreaToDecorationsChanged?.Invoke(true); + if (_isClientAreaExtended) + { + UpdateExtendMargins(); - WindowStateChanged?.Invoke(windowState); + ExtendClientAreaToDecorationsChanged?.Invoke(true); + } } return IntPtr.Zero; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 4b51a9dc42..b8f41a346a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -673,7 +673,7 @@ namespace Avalonia.Win32 TaskBarList.MarkFullscreen(_hwnd, fullscreen); } - private MARGINS UpdateExtendMargins(ExtendClientAreaChromeHints hints) + private MARGINS UpdateExtendMargins() { RECT borderThickness = new RECT(); RECT borderCaptionThickness = new RECT(); @@ -685,7 +685,7 @@ namespace Avalonia.Win32 borderCaptionThickness.left *= -1; borderCaptionThickness.top *= -1; - bool wantsTitleBar = hints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar) || _extendTitleBarHint == -1; + bool wantsTitleBar = _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar) || _extendTitleBarHint == -1; if (!wantsTitleBar) { @@ -702,7 +702,7 @@ namespace Avalonia.Win32 borderCaptionThickness.top = (int)(_extendTitleBarHint * Scaling); } - margins.cyTopHeight = hints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar) ? borderCaptionThickness.top : 1; + margins.cyTopHeight = _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar) ? borderCaptionThickness.top : 1; _extendedMargins = new Thickness(0, borderCaptionThickness.top / Scaling, 0, 0); @@ -718,22 +718,32 @@ namespace Avalonia.Win32 return margins; } - private void ExtendClientArea (ExtendClientAreaChromeHints hints) + private void ExtendClientArea () { - if (!_isClientAreaExtended) + if (DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled) { - if(DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled) - { - return; - } + _isClientAreaExtended = false; + return; + } - var margins = UpdateExtendMargins(hints); + GetWindowRect(_hwnd, out var rcClient); - var hr = DwmExtendFrameIntoClientArea(_hwnd, ref margins); + // Inform the application of the frame change. + SetWindowPos(_hwnd, + IntPtr.Zero, + rcClient.left, rcClient.top, + rcClient.Width, rcClient.Height, + SetWindowPosFlags.SWP_FRAMECHANGED); + + if(_isClientAreaExtended) + { + var margins = UpdateExtendMargins(); - if(!hints.HasFlag(ExtendClientAreaChromeHints.SystemChromeButtons) || - (hints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChromeButtons) && - !hints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar))) + DwmExtendFrameIntoClientArea(_hwnd, ref margins); + + if(!_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemChromeButtons) || + (_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChromeButtons) && + !_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar))) { var style = GetStyle(); @@ -743,13 +753,17 @@ namespace Avalonia.Win32 DisableCloseButton(_hwnd); } + } + else + { + var margins = new MARGINS(); + DwmExtendFrameIntoClientArea(_hwnd, ref margins); - if (hr == 0) - { - _isClientAreaExtended = true; - ExtendClientAreaToDecorationsChanged?.Invoke(true); - } + _offScreenMargin = new Thickness(); + _extendedMargins = new Thickness(); } + + ExtendClientAreaToDecorationsChanged?.Invoke(true); } private void ShowWindow(WindowState state) @@ -1016,7 +1030,9 @@ namespace Avalonia.Win32 public void SetExtendClientAreaToDecorationsHint(bool hint) { - ExtendClientArea(_extendChromeHints); + _isClientAreaExtended = hint; + + ExtendClientArea(); } private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; @@ -1024,12 +1040,16 @@ namespace Avalonia.Win32 public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) { _extendChromeHints = hints; + + ExtendClientArea(); } private double _extendTitleBarHint = -1; public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) { _extendTitleBarHint = titleBarHeight; + + ExtendClientArea(); } public bool IsClientAreaExtendedToDecorations => _isClientAreaExtended; From f7b63f2bc47eb0198a47c9b17d231b38e76b0fca Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 16:26:51 -0300 Subject: [PATCH 27/67] Add a WindowCustomization page to the control catalog. --- samples/ControlCatalog/MainView.xaml | 1 + samples/ControlCatalog/MainWindow.xaml | 8 ++- .../Pages/WindowCustomizationsPage.xaml | 13 ++++ .../Pages/WindowCustomizationsPage.xaml.cs | 19 ++++++ .../ViewModels/MainWindowViewModel.cs | 60 +++++++++++++++++++ 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml create mode 100644 samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 7956ee6169..96e224b031 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -59,6 +59,7 @@ + diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 76422bc130..4c3a00a402 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,7 +7,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}"> + ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}" + ExtendClientAreaChromeHints="{Binding ChromeHints}" + ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" + x:Name="MainWindow" + x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="{x:Null}"> @@ -61,7 +65,7 @@ - + diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml new file mode 100644 index 0000000000..168b504b40 --- /dev/null +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs new file mode 100644 index 0000000000..d8d4f3f371 --- /dev/null +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class WindowCustomizationsPage : UserControl + { + public WindowCustomizationsPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 0257b4ce66..26b7e1bea8 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -3,6 +3,8 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; using Avalonia.Dialogs; +using Avalonia.Platform; +using System; using ReactiveUI; namespace ControlCatalog.ViewModels @@ -62,8 +64,66 @@ namespace ControlCatalog.ViewModels WindowState.Maximized, WindowState.FullScreen, }; + + this.WhenAnyValue(x => x.SystemChromeButtonsEnabled, x => x.SystemTitleBarEnabled) + .Subscribe(x => + { + ChromeHints = ExtendClientAreaChromeHints.NoChrome; + + if(x.Item1) + { + ChromeHints |= ExtendClientAreaChromeHints.SystemChromeButtons; + } + + if(x.Item2) + { + ChromeHints |= ExtendClientAreaChromeHints.SystemTitleBar; + } + }); + } + + private ExtendClientAreaChromeHints _chromeHints; + + public ExtendClientAreaChromeHints ChromeHints + { + get { return _chromeHints; } + set { this.RaiseAndSetIfChanged(ref _chromeHints, value); } + } + + + private bool _extendClientAreaEnabled; + + public bool ExtendClientAreaEnabled + { + get { return _extendClientAreaEnabled; } + set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } } + private bool _systemTitleBarEnabled; + + public bool SystemTitleBarEnabled + { + get { return _systemTitleBarEnabled; } + set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); } + } + + private bool _systemChromeButtonsEnabled; + + public bool SystemChromeButtonsEnabled + { + get { return _systemChromeButtonsEnabled; } + set { this.RaiseAndSetIfChanged(ref _systemChromeButtonsEnabled, value); } + } + + private double _titleBarHeight; + + public double TitleBarHeight + { + get { return _titleBarHeight; } + set { this.RaiseAndSetIfChanged(ref _titleBarHeight, value); } + } + + public WindowState WindowState { get { return _windowState; } From 743967bcf8ed74ae89f0b8603b5566054e1cfffa Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 16:27:33 -0300 Subject: [PATCH 28/67] Update when caption buttons are turned on or off. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index b8f41a346a..ba321e8d77 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -753,6 +753,16 @@ namespace Avalonia.Win32 DisableCloseButton(_hwnd); } + else + { + var style = GetStyle(); + + style |= (WindowStyles.WS_MINIMIZEBOX | WindowStyles.WS_MAXIMIZEBOX | WindowStyles.WS_SYSMENU); + + SetStyle(style); + + EnableCloseButton(_hwnd); + } } else { @@ -761,6 +771,14 @@ namespace Avalonia.Win32 _offScreenMargin = new Thickness(); _extendedMargins = new Thickness(); + + var style = GetStyle(); + + style |= (WindowStyles.WS_MINIMIZEBOX | WindowStyles.WS_MAXIMIZEBOX | WindowStyles.WS_SYSMENU); + + SetStyle(style); + + EnableCloseButton(_hwnd); } ExtendClientAreaToDecorationsChanged?.Invoke(true); From 7cf0282ef8dd5d638666f4782109d021bedf0b00 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 16:27:57 -0300 Subject: [PATCH 29/67] NoChome is 0 --- src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs index af33e3093b..e98124b2d8 100644 --- a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs +++ b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs @@ -4,9 +4,9 @@ namespace Avalonia.Platform { [Flags] public enum ExtendClientAreaChromeHints - { - Default = SystemTitleBar | SystemChromeButtons, + { NoChrome, + Default = SystemTitleBar | SystemChromeButtons, SystemTitleBar = 0x01, SystemChromeButtons = 0x02, ManagedChromeButtons = 0x04, From 04029050e35025d72e825f19904d4a434c3a06f7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 16:36:49 -0300 Subject: [PATCH 30/67] Move transparency to window customizations page. --- samples/ControlCatalog/MainView.xaml.cs | 7 ------- samples/ControlCatalog/MainWindow.xaml | 1 + .../Pages/WindowCustomizationsPage.xaml | 5 +++++ .../ControlCatalog/ViewModels/MainWindowViewModel.cs | 12 ++++++++++++ 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index d6d4a71ad3..b0c205246e 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -58,13 +58,6 @@ namespace ControlCatalog if (VisualRoot is Window window) window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex; }; - - var transparencyLevels = this.Find("TransparencyLevels"); - transparencyLevels.SelectionChanged += (sender, e) => - { - if (VisualRoot is Window window) - window.TransparencyLevelHint = (WindowTransparencyLevel)transparencyLevels.SelectedIndex; - }; } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 4c3a00a402..e92339416b 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -10,6 +10,7 @@ ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}" ExtendClientAreaChromeHints="{Binding ChromeHints}" ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" + TransparencyLevelHint="{Binding TransparencyLevel}" x:Name="MainWindow" x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="{x:Null}"> diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index 168b504b40..44973f5b79 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -9,5 +9,10 @@ + + None + Transparent + Blur + diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 26b7e1bea8..80231a4c15 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -80,6 +80,18 @@ namespace ControlCatalog.ViewModels ChromeHints |= ExtendClientAreaChromeHints.SystemTitleBar; } }); + + SystemTitleBarEnabled = true; + SystemChromeButtonsEnabled = true; + TitleBarHeight = -1; + } + + private int _transparencyLevel; + + public int TransparencyLevel + { + get { return _transparencyLevel; } + set { this.RaiseAndSetIfChanged(ref _transparencyLevel, value); } } private ExtendClientAreaChromeHints _chromeHints; From 926d016a6737e592b3b1ba2a1da64aab80091222 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 16:55:19 -0300 Subject: [PATCH 31/67] fully working client area extension demo --- samples/ControlCatalog/MainWindow.xaml | 32 ++++++++++++------- .../Pages/WindowCustomizationsPage.xaml | 2 +- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index e92339416b..661975d0b5 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -61,20 +61,28 @@ - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index 44973f5b79..2f9c2d3938 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ControlCatalog.Pages.WindowCustomizationsPage"> - + From c5e16651541e1978e9aa52726fde888dad92979f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 17:57:21 -0300 Subject: [PATCH 32/67] fix layout. --- samples/ControlCatalog/MainWindow.xaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 661975d0b5..a6750f6444 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -66,8 +66,9 @@ - - + + + @@ -78,11 +79,12 @@ - + + - + From ff0f1a37d79f656a43fc95a99e5976ce74610b91 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 21 May 2020 17:58:45 -0300 Subject: [PATCH 33/67] only show titlebar content when in dwm mode. --- samples/ControlCatalog/MainWindow.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index a6750f6444..b110451421 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -80,7 +80,7 @@ - + From bfd2e3fd7eaab1c90b1e45fc2989d298dd057923 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 May 2020 14:38:59 -0300 Subject: [PATCH 34/67] Add managed CaptionButtonsControl. --- .../Chrome/CaptionButtons.cs | 89 +++++++++++++++++++ .../Primitives/ChromeOverlayLayer.cs | 37 ++++++++ .../Primitives/VisualLayerManager.cs | 15 +++- .../Properties/AssemblyInfo.cs | 1 + .../CaptionButtons.xaml | 70 +++++++++++++++ src/Avalonia.Themes.Default/DefaultTheme.xaml | 1 + 6 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls/Chrome/CaptionButtons.cs create mode 100644 src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs create mode 100644 src/Avalonia.Themes.Default/CaptionButtons.xaml diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs new file mode 100644 index 0000000000..20a42aadf6 --- /dev/null +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Text; +using Avalonia.Controls.Primitives; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Chrome +{ + public class CaptionButtons : TemplatedControl + { + private CompositeDisposable _disposables; + private Window _hostWindow; + + public CaptionButtons(Window hostWindow) + { + _hostWindow = hostWindow; + } + + public void Attach() + { + if (_disposables == null) + { + var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); + + layer.Children.Add(this); + + _disposables = new CompositeDisposable + { + _hostWindow.GetObservable(Window.WindowDecorationMarginsProperty) + .Subscribe(x => + { + Height = x.Top; + }), + + + _hostWindow.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty) + .Subscribe(x => InvalidateSize()), + + _hostWindow.GetObservable(Window.OffScreenMarginProperty) + .Subscribe(x => InvalidateSize()), + + _hostWindow.GetObservable(Window.WindowStateProperty) + .Subscribe(x => + { + PseudoClasses.Set(":minimized", x == WindowState.Minimized); + PseudoClasses.Set(":normal", x == WindowState.Normal); + PseudoClasses.Set(":maximized", x == WindowState.Maximized); + PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); + }) + }; + } + } + + void InvalidateSize () + { + Margin = new Thickness(1, _hostWindow.OffScreenMargin.Top, 1, 1); + Height = _hostWindow.WindowDecorationMargins.Top - _hostWindow.OffScreenMargin.Top; + } + + public void Detach() + { + if (_disposables != null) + { + var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); + + layer.Children.Remove(this); + + _disposables.Dispose(); + _disposables = null; + } + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + var closeButton = e.NameScope.Find("PART_CloseButton"); + var restoreButton = e.NameScope.Find("PART_RestoreButton"); + var minimiseButton = e.NameScope.Find("PART_MinimiseButton"); + var fullScreenButton = e.NameScope.Find("PART_FullScreenButton"); + + closeButton.PointerPressed += (sender, e) => _hostWindow.Close(); + restoreButton.PointerPressed += (sender, e) => _hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + minimiseButton.PointerPressed += (sender, e) => _hostWindow.WindowState = WindowState.Minimized; + fullScreenButton.PointerPressed += (sender, e) => _hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen; + } + } +} diff --git a/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs b/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs new file mode 100644 index 0000000000..5deb9ac408 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs @@ -0,0 +1,37 @@ +using System.Linq; +using Avalonia.Rendering; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + public class ChromeOverlayLayer : Canvas, ICustomSimpleHitTest + { + public Size AvailableSize { get; private set; } + + public static ChromeOverlayLayer GetOverlayLayer(IVisual visual) + { + foreach (var v in visual.GetVisualAncestors()) + if (v is VisualLayerManager vlm) + if (vlm.OverlayLayer != null) + return vlm.ChromeOverlayLayer; + + if (visual is TopLevel tl) + { + var layers = tl.GetVisualDescendants().OfType().FirstOrDefault(); + return layers?.ChromeOverlayLayer; + } + + return null; + } + + public bool HitTest(Point point) => Children.HitTestCustom(point); + + protected override Size ArrangeOverride(Size finalSize) + { + // We are saving it here since child controls might need to know the entire size of the overlay + // and Bounds won't be updated in time + AvailableSize = finalSize; + return base.ArrangeOverride(finalSize); + } + } +} diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs index 86aeb5c62a..3084d7fa72 100644 --- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -6,7 +6,9 @@ namespace Avalonia.Controls.Primitives public class VisualLayerManager : Decorator { private const int AdornerZIndex = int.MaxValue - 100; - private const int OverlayZIndex = int.MaxValue - 99; + private const int ChromeZIndex = int.MaxValue - 99; + private const int OverlayZIndex = int.MaxValue - 98; + private ILogicalRoot _logicalRoot; private readonly List _layers = new List(); @@ -24,6 +26,17 @@ namespace Avalonia.Controls.Primitives } } + public ChromeOverlayLayer ChromeOverlayLayer + { + get + { + var rv = FindLayer(); + if (rv == null) + AddLayer(rv = new ChromeOverlayLayer(), ChromeZIndex); + return rv; + } + } + public OverlayLayer OverlayLayer { get diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index 060db46212..672fbe294e 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -13,3 +13,4 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Shapes")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Chrome")] diff --git a/src/Avalonia.Themes.Default/CaptionButtons.xaml b/src/Avalonia.Themes.Default/CaptionButtons.xaml new file mode 100644 index 0000000000..0cc0d6b9cd --- /dev/null +++ b/src/Avalonia.Themes.Default/CaptionButtons.xaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 67279fca99..725059c99f 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -9,6 +9,7 @@ + From f1b1db5f56429da6d9be0ef3b1ca2c4bdd5d41be Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 May 2020 14:39:16 -0300 Subject: [PATCH 35/67] Add acrylic blur option to control catalog. --- samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index 2f9c2d3938..f2513da8ec 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -13,6 +13,7 @@ None Transparent Blur + AcrylicBlur From c6b7be7c84f1c7f8683813657925b77b40741467 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 May 2020 15:04:01 -0300 Subject: [PATCH 36/67] insert managed chrome when extend client area is enabled and hint flag is set. --- .../Pages/WindowCustomizationsPage.xaml | 1 + .../ViewModels/MainWindowViewModel.cs | 16 +++++++++- src/Avalonia.Controls/Window.cs | 32 ++++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index f2513da8ec..cea8e4b94e 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -8,6 +8,7 @@ + None diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 80231a4c15..b3ea25e470 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -65,7 +65,7 @@ namespace ControlCatalog.ViewModels WindowState.FullScreen, }; - this.WhenAnyValue(x => x.SystemChromeButtonsEnabled, x => x.SystemTitleBarEnabled) + this.WhenAnyValue(x => x.SystemChromeButtonsEnabled, x=>x.ManagedChromeButtonsEnabled, x => x.SystemTitleBarEnabled) .Subscribe(x => { ChromeHints = ExtendClientAreaChromeHints.NoChrome; @@ -76,6 +76,11 @@ namespace ControlCatalog.ViewModels } if(x.Item2) + { + ChromeHints |= ExtendClientAreaChromeHints.ManagedChromeButtons; + } + + if(x.Item3) { ChromeHints |= ExtendClientAreaChromeHints.SystemTitleBar; } @@ -127,6 +132,15 @@ namespace ControlCatalog.ViewModels set { this.RaiseAndSetIfChanged(ref _systemChromeButtonsEnabled, value); } } + private bool _managedChromeButtonsEnabled; + + public bool ManagedChromeButtonsEnabled + { + get { return _managedChromeButtonsEnabled; } + set { this.RaiseAndSetIfChanged(ref _managedChromeButtonsEnabled, value); } + } + + private double _titleBarHeight; public double TitleBarHeight diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 9e7b9ea288..adade6dc75 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; +using Avalonia.Controls.Chrome; using Avalonia.Controls.Platform; using Avalonia.Data; using Avalonia.Input; @@ -70,6 +71,7 @@ namespace Avalonia.Controls public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot { private List _children = new List(); + private CaptionButtons _managedCaptions; private bool _isExtendedIntoWindowDecorations; @@ -203,7 +205,15 @@ namespace Avalonia.Controls (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.SetExtendClientAreaToDecorationsHint((bool)e.NewValue); }); ExtendClientAreaChromeHintsProperty.Changed.AddClassHandler( - (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.SetExtendClientAreaChromeHints((ExtendClientAreaChromeHints)e.NewValue); }); + (w, e) => + { + if (w.PlatformImpl != null) + { + w.PlatformImpl.SetExtendClientAreaChromeHints((ExtendClientAreaChromeHints)e.NewValue); + } + + w.HandleChromeHintsChanged((ExtendClientAreaChromeHints)e.NewValue); + }); ExtendClientAreaTitleBarHeightHintProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.SetExtendClientAreaTitleBarHeightHint((double)e.NewValue); }); @@ -840,6 +850,26 @@ namespace Avalonia.Controls base.HandleResized(clientSize); } + private void HandleChromeHintsChanged (ExtendClientAreaChromeHints hints) + { + if(hints.HasFlag(ExtendClientAreaChromeHints.ManagedChromeButtons)) + { + if(_managedCaptions == null) + { + _managedCaptions = new CaptionButtons(this); + } + + _managedCaptions.Attach(); + } + else + { + if(_managedCaptions != null) + { + _managedCaptions.Detach(); + } + } + } + /// /// Raises the event. /// From c840fb6a6be53697c2e7d5924d25161d0031f42b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 May 2020 16:14:41 -0300 Subject: [PATCH 37/67] When system captions are disabled on win32 and window is maximized border will not be hidden offscreen. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ba321e8d77..4f07159278 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -706,7 +706,11 @@ namespace Avalonia.Win32 _extendedMargins = new Thickness(0, borderCaptionThickness.top / Scaling, 0, 0); - if (WindowState == WindowState.Maximized) + if (WindowState == WindowState.Maximized && !_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemChromeButtons)) + { + _offScreenMargin = new Thickness(); + } + else if (WindowState == WindowState.Maximized) { _offScreenMargin = new Thickness(borderThickness.left / Scaling, borderThickness.top / Scaling, borderThickness.right / Scaling, borderThickness.bottom / Scaling); } From 034af1ed05e41f11d0a55fdc93d42d0d5ad9ead7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 May 2020 17:18:34 -0300 Subject: [PATCH 38/67] Decorations Margin shouldnt include the border margin other wise users have to do subtractions. --- src/Avalonia.Controls/Chrome/CaptionButtons.cs | 2 +- src/Avalonia.Controls/Window.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index 20a42aadf6..556c759825 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls.Chrome void InvalidateSize () { Margin = new Thickness(1, _hostWindow.OffScreenMargin.Top, 1, 1); - Height = _hostWindow.WindowDecorationMargins.Top - _hostWindow.OffScreenMargin.Top; + Height = _hostWindow.WindowDecorationMargins.Top; } public void Detach() diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index adade6dc75..51defce27a 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -543,7 +543,11 @@ namespace Avalonia.Controls { IsExtendedIntoWindowDecorations = isExtended; - WindowDecorationMargins = PlatformImpl.ExtendedMargins; + WindowDecorationMargins = new Thickness( + PlatformImpl.ExtendedMargins.Left, + PlatformImpl.ExtendedMargins.Top - PlatformImpl.OffScreenMargin.Top, + PlatformImpl.ExtendedMargins.Right, + PlatformImpl.ExtendedMargins.Bottom); OffScreenMargin = PlatformImpl.OffScreenMargin; } From 212371ce273ae6a21207d166177abda56a70006f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 May 2020 17:27:33 -0300 Subject: [PATCH 39/67] extend api only allows to turn titlebar on or off to maintain window integrity, and decoration margins dont include border margins. --- .../Platform/ExtendClientAreaChromeHints.cs | 7 ++- src/Avalonia.Controls/Window.cs | 6 +-- .../CaptionButtons.xaml | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 43 ++----------------- 4 files changed, 9 insertions(+), 49 deletions(-) diff --git a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs index e98124b2d8..075e4ba436 100644 --- a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs +++ b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs @@ -6,11 +6,10 @@ namespace Avalonia.Platform public enum ExtendClientAreaChromeHints { NoChrome, - Default = SystemTitleBar | SystemChromeButtons, + Default = SystemTitleBar, SystemTitleBar = 0x01, - SystemChromeButtons = 0x02, - ManagedChromeButtons = 0x04, - PreferSystemChromeButtons = 0x08, + ManagedChromeButtons = 0x02, + PreferSystemChromeButtons = 0x04, AdaptiveChromeWithTitleBar = SystemTitleBar | PreferSystemChromeButtons, AdaptiveChromeWithoutTitleBar = PreferSystemChromeButtons, } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 51defce27a..adade6dc75 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -543,11 +543,7 @@ namespace Avalonia.Controls { IsExtendedIntoWindowDecorations = isExtended; - WindowDecorationMargins = new Thickness( - PlatformImpl.ExtendedMargins.Left, - PlatformImpl.ExtendedMargins.Top - PlatformImpl.OffScreenMargin.Top, - PlatformImpl.ExtendedMargins.Right, - PlatformImpl.ExtendedMargins.Bottom); + WindowDecorationMargins = PlatformImpl.ExtendedMargins; OffScreenMargin = PlatformImpl.OffScreenMargin; } diff --git a/src/Avalonia.Themes.Default/CaptionButtons.xaml b/src/Avalonia.Themes.Default/CaptionButtons.xaml index 0cc0d6b9cd..8ee8a3b0dd 100644 --- a/src/Avalonia.Themes.Default/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Default/CaptionButtons.xaml @@ -5,7 +5,7 @@ - + - @@ -27,13 +24,13 @@ - + - + @@ -42,13 +39,13 @@ - + - + diff --git a/src/Avalonia.Themes.Default/TitleBar.xaml b/src/Avalonia.Themes.Default/TitleBar.xaml new file mode 100644 index 0000000000..856b90857c --- /dev/null +++ b/src/Avalonia.Themes.Default/TitleBar.xaml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/CaptionButtons.xaml new file mode 100644 index 0000000000..20efcafcfd --- /dev/null +++ b/src/Avalonia.Themes.Fluent/CaptionButtons.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Styles> diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index a20f075e21..90a1cc3273 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -7,7 +7,8 @@ - + + @@ -35,6 +36,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/TitleBar.xaml b/src/Avalonia.Themes.Fluent/TitleBar.xaml new file mode 100644 index 0000000000..6512ee342c --- /dev/null +++ b/src/Avalonia.Themes.Fluent/TitleBar.xaml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index fee6bb90bf..d4d7528ef4 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -90,6 +90,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCHITTEST: if (lRet == IntPtr.Zero) { + if(WindowState == WindowState.FullScreen) + { + return (IntPtr)HitTestValues.HTCLIENT; + } var hittestResult = HitTestNCA(hWnd, wParam, lParam); lRet = (IntPtr)hittestResult; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f232177edf..d35e961a4b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -188,6 +188,11 @@ namespace Avalonia.Win32 { get { + if(_isFullScreenActive) + { + return WindowState.FullScreen; + } + var placement = default(WINDOWPLACEMENT); GetWindowPlacement(_hwnd, ref placement); From 7aefda0c24cf71d0228e0e9796dbaffc54b8d68e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Jun 2020 15:04:21 -0300 Subject: [PATCH 62/67] fix xaml error. --- src/Avalonia.Themes.Fluent/CaptionButtons.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/CaptionButtons.xaml index 20efcafcfd..e5177bbdad 100644 --- a/src/Avalonia.Themes.Fluent/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Fluent/CaptionButtons.xaml @@ -64,4 +64,4 @@ - Styles> + From a49fc286b66d4bbebbccf88c0df4d0c1a3f62d20 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Jun 2020 15:08:48 -0300 Subject: [PATCH 63/67] hide certain caption buttons in fullscreen mode. --- src/Avalonia.Themes.Fluent/CaptionButtons.xaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Themes.Fluent/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/CaptionButtons.xaml index e5177bbdad..7ed27a1747 100644 --- a/src/Avalonia.Themes.Fluent/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Fluent/CaptionButtons.xaml @@ -64,4 +64,7 @@ + From 5a09fd63bc3554d6780eb6709c813e18853dc847 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Jun 2020 15:49:36 -0300 Subject: [PATCH 64/67] fix animations --- .../ControlCatalog/ViewModels/MainWindowViewModel.cs | 10 ++++++---- src/Avalonia.Controls/Chrome/TitleBar.cs | 3 +++ src/Avalonia.Themes.Fluent/TitleBar.xaml | 7 ++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 939f44d772..27ea1ffb15 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -68,22 +68,24 @@ namespace ControlCatalog.ViewModels this.WhenAnyValue(x => x.SystemChromeButtonsEnabled, x=>x.ManagedChromeButtonsEnabled, x => x.SystemTitleBarEnabled) .Subscribe(x => { - ChromeHints = ExtendClientAreaChromeHints.NoChrome | ExtendClientAreaChromeHints.OSXThickTitleBar; + var hints = ExtendClientAreaChromeHints.NoChrome | ExtendClientAreaChromeHints.OSXThickTitleBar; if(x.Item1) { - ChromeHints |= ExtendClientAreaChromeHints.SystemChromeButtons; + hints |= ExtendClientAreaChromeHints.SystemChromeButtons; } if(x.Item2) { - ChromeHints |= ExtendClientAreaChromeHints.ManagedChromeButtons; + hints |= ExtendClientAreaChromeHints.ManagedChromeButtons; } if(x.Item3) { - ChromeHints |= ExtendClientAreaChromeHints.SystemTitleBar; + hints |= ExtendClientAreaChromeHints.SystemTitleBar; } + + ChromeHints = hints; }); SystemTitleBarEnabled = true; diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index 23eefb5d1b..d4ac5cd855 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -2,6 +2,7 @@ using System.Reactive.Disposables; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.LogicalTree; using Avalonia.Media; namespace Avalonia.Controls.Chrome @@ -63,6 +64,8 @@ namespace Avalonia.Controls.Chrome PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); }) }; + + _captionButtons?.Attach(_hostWindow); } } diff --git a/src/Avalonia.Themes.Fluent/TitleBar.xaml b/src/Avalonia.Themes.Fluent/TitleBar.xaml index 6512ee342c..f7d9f1df10 100644 --- a/src/Avalonia.Themes.Fluent/TitleBar.xaml +++ b/src/Avalonia.Themes.Fluent/TitleBar.xaml @@ -30,15 +30,13 @@ - - - + - - - - From 94da8ef9720fb14f3305ee2ce1ed6bd0e6f71367 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 9 Jun 2020 13:35:58 -0300 Subject: [PATCH 66/67] polish managed titlebar. --- .../Chrome/CaptionButtons.cs | 18 ------- src/Avalonia.Controls/Chrome/TitleBar.cs | 34 +++++-------- src/Avalonia.Themes.Fluent/TitleBar.xaml | 12 +++-- src/Windows/Avalonia.Win32/WindowImpl.cs | 48 +++++++++++-------- 4 files changed, 49 insertions(+), 63 deletions(-) diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index 618c3c4fb2..99db4e550b 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -20,18 +20,6 @@ namespace Avalonia.Controls.Chrome _disposables = new CompositeDisposable { - _hostWindow.GetObservable(Window.WindowDecorationMarginsProperty) - .Subscribe(x => - { - Height = x.Top; - }), - - _hostWindow.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty) - .Subscribe(x => InvalidateSize()), - - _hostWindow.GetObservable(Window.OffScreenMarginProperty) - .Subscribe(x => InvalidateSize()), - _hostWindow.GetObservable(Window.WindowStateProperty) .Subscribe(x => { @@ -44,12 +32,6 @@ namespace Avalonia.Controls.Chrome } } - void InvalidateSize () - { - Margin = new Thickness(1, _hostWindow.OffScreenMargin.Top, 1, 1); - Height = _hostWindow.WindowDecorationMargins.Top; - } - public void Detach() { if (_disposables != null) diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index d4ac5cd855..b0ae25892e 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -18,21 +18,6 @@ namespace Avalonia.Controls.Chrome _hostWindow = hostWindow; } - public TitleBar() - { - - } - - protected override Size MeasureOverride(Size availableSize) - { - return base.MeasureOverride(availableSize); - } - - protected override Size ArrangeOverride(Size finalSize) - { - return base.ArrangeOverride(finalSize); - } - public void Attach() { if (_disposables == null) @@ -44,10 +29,7 @@ namespace Avalonia.Controls.Chrome _disposables = new CompositeDisposable { _hostWindow.GetObservable(Window.WindowDecorationMarginsProperty) - .Subscribe(x => - { - Height = x.Top; - }), + .Subscribe(x => InvalidateSize()), _hostWindow.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty) .Subscribe(x => InvalidateSize()), @@ -71,8 +53,16 @@ namespace Avalonia.Controls.Chrome void InvalidateSize() { - Margin = new Thickness(1, _hostWindow.OffScreenMargin.Top, 1, 1); - Height = _hostWindow.WindowDecorationMargins.Top; + Margin = new Thickness( + _hostWindow.OffScreenMargin.Left, + _hostWindow.OffScreenMargin.Top, + _hostWindow.OffScreenMargin.Right, + _hostWindow.OffScreenMargin.Bottom); + + if (_hostWindow.WindowState != WindowState.FullScreen) + { + Height = _hostWindow.WindowDecorationMargins.Top; + } } public void Detach() @@ -94,7 +84,7 @@ namespace Avalonia.Controls.Chrome { base.OnApplyTemplate(e); - _captionButtons = e.NameScope.Find("PART_CaptionButtons"); + _captionButtons = e.NameScope.Find("PART_CaptionButtons"); _captionButtons.Attach(_hostWindow); } diff --git a/src/Avalonia.Themes.Fluent/TitleBar.xaml b/src/Avalonia.Themes.Fluent/TitleBar.xaml index 5083f4c15a..db4dbe95a2 100644 --- a/src/Avalonia.Themes.Fluent/TitleBar.xaml +++ b/src/Avalonia.Themes.Fluent/TitleBar.xaml @@ -5,23 +5,27 @@ + + diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index d35e961a4b..4a5c946cc3 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -676,6 +676,8 @@ namespace Avalonia.Win32 } TaskBarList.MarkFullscreen(_hwnd, fullscreen); + + ExtendClientArea(); } private MARGINS UpdateExtendMargins() @@ -729,33 +731,41 @@ namespace Avalonia.Win32 { _isClientAreaExtended = false; return; - } - - GetWindowRect(_hwnd, out var rcClient); + } - // Inform the application of the frame change. - SetWindowPos(_hwnd, - IntPtr.Zero, - rcClient.left, rcClient.top, - rcClient.Width, rcClient.Height, - SetWindowPosFlags.SWP_FRAMECHANGED); - - if(_isClientAreaExtended) + if (!_isClientAreaExtended || WindowState == WindowState.FullScreen) { - var margins = UpdateExtendMargins(); - - DwmExtendFrameIntoClientArea(_hwnd, ref margins); + _extendedMargins = new Thickness(0, 0, 0, 0); + _offScreenMargin = new Thickness(); } else { - var margins = new MARGINS(); - DwmExtendFrameIntoClientArea(_hwnd, ref margins); + GetWindowRect(_hwnd, out var rcClient); - _offScreenMargin = new Thickness(); - _extendedMargins = new Thickness(); + // Inform the application of the frame change. + SetWindowPos(_hwnd, + IntPtr.Zero, + rcClient.left, rcClient.top, + rcClient.Width, rcClient.Height, + SetWindowPosFlags.SWP_FRAMECHANGED); + + if (_isClientAreaExtended) + { + var margins = UpdateExtendMargins(); + + DwmExtendFrameIntoClientArea(_hwnd, ref margins); + } + else + { + var margins = new MARGINS(); + DwmExtendFrameIntoClientArea(_hwnd, ref margins); + + _offScreenMargin = new Thickness(); + _extendedMargins = new Thickness(); + } } - ExtendClientAreaToDecorationsChanged?.Invoke(true); + ExtendClientAreaToDecorationsChanged?.Invoke(_isClientAreaExtended); } private void ShowWindow(WindowState state) From 158137eefb9084437f66e746662e8d6ba5967dc5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 10 Jun 2020 20:57:56 -0300 Subject: [PATCH 67/67] fix titlebar sizing. --- src/Avalonia.Controls/Chrome/TitleBar.cs | 5 +++++ src/Avalonia.Themes.Fluent/TitleBar.xaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index b0ae25892e..aeac9b76d4 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -62,6 +62,11 @@ namespace Avalonia.Controls.Chrome if (_hostWindow.WindowState != WindowState.FullScreen) { Height = _hostWindow.WindowDecorationMargins.Top; + + if (_captionButtons != null) + { + _captionButtons.Height = Height; + } } } diff --git a/src/Avalonia.Themes.Fluent/TitleBar.xaml b/src/Avalonia.Themes.Fluent/TitleBar.xaml index db4dbe95a2..3c194e5d44 100644 --- a/src/Avalonia.Themes.Fluent/TitleBar.xaml +++ b/src/Avalonia.Themes.Fluent/TitleBar.xaml @@ -15,7 +15,7 @@ - +