From 0b1b3914b347c616ec63265942f67f3f547f4dba Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 27 Apr 2020 12:47:37 +0200 Subject: [PATCH 01/70] More text rendering improvements --- src/Avalonia.Visuals/Media/FontManager.cs | 26 +++- .../Media/Fonts/FamilyNameCollection.cs | 3 +- src/Avalonia.Visuals/Media/Fonts/FontKey.cs | 10 +- .../Media/TextFormatting/TextFormatter.cs | 2 +- .../Unicode/CodepointEnumerator.cs | 2 +- .../Unicode/UnicodeGeneralCategory.cs | 44 ------- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 67 ++++++---- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 46 +++++-- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 3 +- src/Skia/Avalonia.Skia/GlyphRunImpl.cs | 9 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 116 +++++++++--------- .../Avalonia.Skia/SKTypefaceCollection.cs | 4 +- .../SKTypefaceCollectionCache.cs | 2 +- .../Media/FontManagerImpl.cs | 2 +- .../CustomFontManagerImpl.cs | 48 ++++++-- .../FontManagerImplTests.cs | 13 ++ .../TextLayoutTests.cs | 24 +++- .../Avalonia.UnitTests/MockFontManagerImpl.cs | 11 +- .../Media/FontManagerTests.cs | 12 +- 19 files changed, 271 insertions(+), 173 deletions(-) delete mode 100644 src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index 2de629432c..f9410afe6a 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -23,6 +23,11 @@ namespace Avalonia.Media DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName(); + if (string.IsNullOrEmpty(DefaultFontFamilyName)) + { + throw new InvalidOperationException("Default font family name can't be null or empty."); + } + _defaultFontFamily = new FontFamily(DefaultFontFamilyName); } @@ -39,7 +44,8 @@ namespace Avalonia.Media var fontManagerImpl = AvaloniaLocator.Current.GetService(); - if (fontManagerImpl == null) throw new InvalidOperationException("No font manager implementation was registered."); + if (fontManagerImpl == null) + throw new InvalidOperationException("No font manager implementation was registered."); current = new FontManager(fontManagerImpl); @@ -87,7 +93,7 @@ namespace Avalonia.Media fontFamily = _defaultFontFamily; } - var key = new FontKey(fontFamily, fontWeight, fontStyle); + var key = new FontKey(fontFamily.Name, fontWeight, fontStyle); if (_typefaceCache.TryGetValue(key, out var typeface)) { @@ -126,9 +132,21 @@ namespace Avalonia.Media FontStyle fontStyle = FontStyle.Normal, FontFamily fontFamily = null, CultureInfo culture = null) { - return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? - _typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) : + foreach (var cachedTypeface in _typefaceCache.Values) + { + // First try to find a cached typeface by style and weight to avoid redundant glyph index lookup. + if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight + && cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0) + { + return cachedTypeface; + } + } + + var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? + _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) : null; + + return matchedTypeface; } } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index a9ea322d76..96312a5466 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Text; using Avalonia.Utilities; @@ -21,7 +20,7 @@ namespace Avalonia.Media.Fonts throw new ArgumentNullException(nameof(familyNames)); } - Names = familyNames.Split(',').Select(x => x.Trim()).ToArray(); + Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim()); PrimaryFamilyName = Names[0]; diff --git a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs index 1f1e9b067d..579a229fd3 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs @@ -4,20 +4,20 @@ namespace Avalonia.Media.Fonts { public readonly struct FontKey : IEquatable { - public readonly FontFamily FontFamily; + public readonly string FamilyName; public readonly FontStyle Style; public readonly FontWeight Weight; - public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style) + public FontKey(string familyName, FontWeight weight, FontStyle style) { - FontFamily = fontFamily; + FamilyName = familyName; Style = style; Weight = weight; } public override int GetHashCode() { - var hash = FontFamily.GetHashCode(); + var hash = FamilyName.GetHashCode(); hash = hash * 31 + (int)Style; hash = hash * 31 + (int)Weight; @@ -32,7 +32,7 @@ namespace Avalonia.Media.Fonts public bool Equals(FontKey other) { - return FontFamily == other.FontFamily && + return FamilyName == other.FamilyName && Style == other.Style && Weight == other.Weight; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs index 7956c5f260..7da39dc5dc 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs @@ -66,7 +66,7 @@ namespace Avalonia.Media.TextFormatting //ToDo: Fix FontFamily fallback currentTypeface = - FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style); + FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultStyle.TextFormat.Typeface.FontFamily); if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs index 9c20efd867..2ff4952cab 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -2,7 +2,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { - internal ref struct CodepointEnumerator + public ref struct CodepointEnumerator { private ReadOnlySlice _text; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs deleted file mode 100644 index 3385116f26..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace Avalonia.Media.TextFormatting.Unicode -{ - public enum UnicodeGeneralCategory : byte - { - Other, //C# Cc | Cf | Cn | Co | Cs - Control, //Cc - Format, //Cf - Unassigned, //Cn - PrivateUse, //Co - Surrogate, //Cs - Letter, //L# Ll | Lm | Lo | Lt | Lu - CasedLetter, //LC# Ll | Lt | Lu - LowercaseLetter, //Ll - ModifierLetter, //Lm - OtherLetter, //Lo - TitlecaseLetter, //Lt - UppercaseLetter, //Lu - Mark, //M - SpacingMark, //Mc - EnclosingMark, //Me - NonspacingMark, //Mn - Number, //N# Nd | Nl | No - DecimalNumber, //Nd - LetterNumber, //Nl - OtherNumber, //No - Punctuation, //P - ConnectorPunctuation, //Pc - DashPunctuation, //Pd - ClosePunctuation, //Pe - FinalPunctuation, //Pf - InitialPunctuation, //Pi - OtherPunctuation, //Po - OpenPunctuation, //Ps - Symbol, //S# Sc | Sk | Sm | So - CurrencySymbol, //Sc - ModifierSymbol, //Sk - MathSymbol, //Sm - OtherSymbol, //So - Separator, //Z# Zl | Zp | Zs - LineSeparator, //Zl - ParagraphSeparator, //Zp - SpaceSeparator, //Zs - } -} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 4bec7b3f56..9f99ed3cef 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -30,6 +30,10 @@ namespace Avalonia.Skia private Matrix _currentTransform; private GRContext _grContext; private bool _disposed; + + private readonly SKPaint _strokePaint = new SKPaint(); + private readonly SKPaint _fillPaint = new SKPaint(); + /// /// Context create info. /// @@ -153,7 +157,7 @@ namespace Avalonia.Skia /// public void DrawLine(IPen pen, Point p1, Point p2) { - using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) + using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint); } @@ -165,8 +169,8 @@ namespace Avalonia.Skia var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; - using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) - using (var stroke = pen?.Brush != null ? CreatePaint(pen, size) : default(PaintWrapper)) + using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper)) + using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper)) { if (fill.Paint != null) { @@ -188,7 +192,7 @@ namespace Avalonia.Skia if (brush != null) { - using (var paint = CreatePaint(brush, rect.Size)) + using (var paint = CreatePaint(_fillPaint, brush, rect.Size)) { if (isRounded) { @@ -204,7 +208,7 @@ namespace Avalonia.Skia if (pen?.Brush != null) { - using (var paint = CreatePaint(pen, rect.Size)) + using (var paint = CreatePaint(_strokePaint, pen, rect.Size)) { if (isRounded) { @@ -222,7 +226,7 @@ namespace Avalonia.Skia /// public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { - using (var paint = CreatePaint(foreground, text.Bounds.Size)) + using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size)) { var textImpl = (FormattedTextImpl) text; textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); @@ -232,14 +236,14 @@ namespace Avalonia.Skia /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) { - using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; - paint.ApplyTo(glyphRunImpl.Paint); + ConfigureTextRendering(paintWrapper); Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X, - (float)baselineOrigin.Y, glyphRunImpl.Paint); + (float)baselineOrigin.Y, paintWrapper.Paint); } } @@ -323,7 +327,7 @@ namespace Avalonia.Skia var paint = new SKPaint(); Canvas.SaveLayer(paint); - _maskStack.Push(CreatePaint(mask, bounds.Size)); + _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true)); } /// @@ -364,6 +368,15 @@ namespace Avalonia.Skia } } + internal void ConfigureTextRendering(PaintWrapper wrapper) + { + var paint = wrapper.Paint; + + paint.IsEmbeddedBitmapText = true; + paint.SubpixelText = true; + paint.LcdRenderText = _canTextUseLcdRendering; + } + /// /// Configure paint wrapper for using gradient brush. /// @@ -514,17 +527,16 @@ namespace Avalonia.Skia /// /// Creates paint wrapper for given brush. /// + /// The paint to wrap. /// Source brush. /// Target size. + /// Optional dispose of the supplied paint. /// Paint wrapper for given brush. - internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) + internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false) { - var paint = new SKPaint - { - IsAntialias = true - }; + var paintWrapper = new PaintWrapper(paint, disposePaint); - var paintWrapper = new PaintWrapper(paint); + paint.IsAntialias = true; double opacity = brush.Opacity * _currentOpacity; @@ -572,10 +584,12 @@ namespace Avalonia.Skia /// /// Creates paint wrapper for given pen. /// + /// The paint to wrap. /// Source pen. /// Target size. + /// Optional dispose of the supplied paint. /// - private PaintWrapper CreatePaint(IPen pen, Size targetSize) + private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false) { // In Skia 0 thickness means - use hairline rendering // and for us it means - there is nothing rendered. @@ -584,8 +598,7 @@ namespace Avalonia.Skia return default; } - var rv = CreatePaint(pen.Brush, targetSize); - var paint = rv.Paint; + var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint); paint.IsStroke = true; paint.StrokeWidth = (float) pen.Thickness; @@ -668,7 +681,7 @@ namespace Avalonia.Skia /// /// Skia cached paint state. /// - private struct PaintState : IDisposable + private readonly struct PaintState : IDisposable { private readonly SKColor _color; private readonly SKShader _shader; @@ -696,14 +709,16 @@ namespace Avalonia.Skia { //We are saving memory allocations there public readonly SKPaint Paint; + private readonly bool _disposePaint; private IDisposable _disposable1; private IDisposable _disposable2; private IDisposable _disposable3; - public PaintWrapper(SKPaint paint) + public PaintWrapper(SKPaint paint, bool disposePaint) { Paint = paint; + _disposePaint = disposePaint; _disposable1 = null; _disposable2 = null; @@ -751,7 +766,15 @@ namespace Avalonia.Skia /// public void Dispose() { - Paint?.Dispose(); + if (_disposePaint) + { + Paint?.Dispose(); + } + else + { + Paint?.Reset(); + } + _disposable1?.Dispose(); _disposable2?.Dispose(); _disposable3?.Dispose(); diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 53aa6a147c..415a89e1c1 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -32,6 +32,27 @@ namespace Avalonia.Skia public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { + SKFontStyle skFontStyle; + + switch (fontWeight) + { + case FontWeight.Normal when fontStyle == FontStyle.Normal: + skFontStyle = SKFontStyle.Normal; + break; + case FontWeight.Normal when fontStyle == FontStyle.Italic: + skFontStyle = SKFontStyle.Italic; + break; + case FontWeight.Bold when fontStyle == FontStyle.Normal: + skFontStyle = SKFontStyle.Bold; + break; + case FontWeight.Bold when fontStyle == FontStyle.Italic: + skFontStyle = SKFontStyle.BoldItalic; + break; + default: + skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle); + break; + } + if (culture == null) { culture = CultureInfo.CurrentUICulture; @@ -45,31 +66,32 @@ namespace Avalonia.Skia t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; - if (fontFamily != null) + if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks) { - foreach (var familyName in fontFamily.FamilyNames) + var familyNames = fontFamily.FamilyNames; + + for (var i = 1; i < familyNames.Count; i++) { - var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); + var skTypeface = + _skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint); if (skTypeface == null) { continue; } - fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle); + fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); return true; } } else { - var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); + var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint); if (skTypeface != null) { - fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle); + fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); return true; } @@ -82,7 +104,7 @@ namespace Avalonia.Skia public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) { - var skTypeface = SKTypeface.Default; + SKTypeface skTypeface = null; if (typeface.FontFamily.Key == null) { @@ -109,6 +131,12 @@ namespace Avalonia.Skia skTypeface = fontCollection.Get(typeface); } + if (skTypeface == null) + { + throw new InvalidOperationException( + $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); + } + return new GlyphTypefaceImpl(skTypeface); } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index d0157815a9..6022e7a552 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -266,7 +266,8 @@ namespace Avalonia.Skia if (fb != null) { //TODO: figure out how to get the brush size - currentWrapper = context.CreatePaint(fb, new Size()); + currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb, + new Size()); } else { diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 7c3f718f9e..0fdea5ed40 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -7,17 +7,11 @@ namespace Avalonia.Skia /// public class GlyphRunImpl : IGlyphRunImpl { - public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob) + public GlyphRunImpl(SKTextBlob textBlob) { - Paint = paint; TextBlob = textBlob; } - /// - /// Gets the paint to draw with. - /// - public SKPaint Paint { get; } - /// /// Gets the text blob to draw. /// @@ -26,7 +20,6 @@ namespace Avalonia.Skia void IDisposable.Dispose() { TextBlob.Dispose(); - Paint.Dispose(); } } } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index f1df6a804e..46872f903e 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -149,6 +149,16 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format); } + private static readonly SKPaint s_paint = new SKPaint + { + TextEncoding = SKTextEncoding.GlyphId, + IsAntialias = true, + IsStroke = false, + SubpixelText = true + }; + + private static readonly SKTextBlobBuilder s_textBlobBuilder = new SKTextBlobBuilder(); + /// public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) { @@ -158,92 +168,84 @@ namespace Avalonia.Skia var typeface = glyphTypeface.Typeface; - var paint = new SKPaint - { - TextSize = (float)glyphRun.FontRenderingEmSize, - Typeface = typeface, - TextEncoding = SKTextEncoding.GlyphId, - IsAntialias = true, - IsStroke = false, - SubpixelText = true - }; - - using (var textBlobBuilder = new SKTextBlobBuilder()) - { - SKTextBlob textBlob; + s_paint.TextSize = (float)glyphRun.FontRenderingEmSize; + s_paint.Typeface = typeface; - width = 0; - var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); + SKTextBlob textBlob; - if (glyphRun.GlyphOffsets.IsEmpty) - { - if (glyphTypeface.IsFixedPitch) - { - textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span); + width = 0; - textBlob = textBlobBuilder.Build(); + var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); - width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length; - } - else - { - var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0); - - var positions = buffer.GetPositionSpan(); - - for (var i = 0; i < count; i++) - { - positions[i] = (float)width; - - if (glyphRun.GlyphAdvances.IsEmpty) - { - width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; - } - else - { - width += glyphRun.GlyphAdvances[i]; - } - } + if (glyphRun.GlyphOffsets.IsEmpty) + { + if (glyphTypeface.IsFixedPitch) + { + s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span); - buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); + textBlob = s_textBlobBuilder.Build(); - textBlob = textBlobBuilder.Build(); - } + width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length; } else { - var buffer = textBlobBuilder.AllocatePositionedRun(paint, count); + var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0); - var glyphPositions = buffer.GetPositionSpan(); - - var currentX = 0.0; + var positions = buffer.GetPositionSpan(); for (var i = 0; i < count; i++) { - var glyphOffset = glyphRun.GlyphOffsets[i]; - - glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y); + positions[i] = (float)width; if (glyphRun.GlyphAdvances.IsEmpty) { - currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; + width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; } else { - currentX += glyphRun.GlyphAdvances[i]; + width += glyphRun.GlyphAdvances[i]; } } buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); - width = currentX; + textBlob = s_textBlobBuilder.Build(); + } + } + else + { + var buffer = s_textBlobBuilder.AllocatePositionedRun(s_paint, count); + + var glyphPositions = buffer.GetPositionSpan(); + + var currentX = 0.0; + + for (var i = 0; i < count; i++) + { + var glyphOffset = glyphRun.GlyphOffsets[i]; + + glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y); - textBlob = textBlobBuilder.Build(); + if (glyphRun.GlyphAdvances.IsEmpty) + { + currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; + } + else + { + currentX += glyphRun.GlyphAdvances[i]; + } } - return new GlyphRunImpl(paint, textBlob); + buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); + + width = currentX; + + textBlob = s_textBlobBuilder.Build(); } + + return new GlyphRunImpl(textBlob); + } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index ce82835968..7aea90e61e 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -19,7 +19,7 @@ namespace Avalonia.Skia public SKTypeface Get(Typeface typeface) { - var key = new FontKey(typeface.FontFamily, typeface.Weight, typeface.Style); + var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style); return GetNearestMatch(_typefaces, key); } @@ -49,7 +49,7 @@ namespace Avalonia.Skia if (keys.Length == 0) { - return SKTypeface.Default; + return null; } key = keys[0]; diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 4f04d25dee..a9aed80a04 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -54,7 +54,7 @@ namespace Avalonia.Skia continue; } - var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); + var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); typeFaceCollection.AddTypeface(key, typeface); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 8ba46dcbce..253a373106 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -50,7 +50,7 @@ namespace Avalonia.Direct2D1.Media var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle); + fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle); return true; } diff --git a/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs index aef2423f8a..8d64190ebd 100644 --- a/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs @@ -11,10 +11,11 @@ namespace Avalonia.Skia.UnitTests public class CustomFontManagerImpl : IFontManagerImpl { private readonly Typeface[] _customTypefaces; + private readonly string _defaultFamilyName; private readonly Typeface _defaultTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); - private readonly Typeface _italicTypeface = + private readonly Typeface _italicTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans"); private readonly Typeface _emojiTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji"); @@ -22,11 +23,12 @@ namespace Avalonia.Skia.UnitTests public CustomFontManagerImpl() { _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface }; + _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName; } public string GetDefaultFontFamilyName() { - return _defaultTypeface.FontFamily.ToString(); + return _defaultFamilyName; } public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) @@ -34,39 +36,65 @@ namespace Avalonia.Skia.UnitTests return _customTypefaces.Select(x => x.FontFamily.Name); } + private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; + public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { foreach (var customTypeface in _customTypefaces) { if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0) + { continue; - fontKey = new FontKey(customTypeface.FontFamily, fontWeight, fontStyle); + } + + fontKey = new FontKey(customTypeface.FontFamily.Name, fontWeight, fontStyle); return true; } - var fallback = SKFontManager.Default.MatchCharacter(codepoint); + var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight, + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint); - fontKey = new FontKey(fallback?.FamilyName ?? SKTypeface.Default.FamilyName, fontWeight, fontStyle); + fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontWeight, fontStyle); return true; } public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) { + SKTypeface skTypeface; + switch (typeface.FontFamily.Name) { case "Twitter Color Emoji": + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily); + skTypeface = typefaceCollection.Get(typeface); + break; + } + case "Noto Sans": + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily); + skTypeface = typefaceCollection.Get(typeface); + break; + } case "Noto Mono": - var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); - var skTypeface = typefaceCollection.Get(typeface); - return new GlyphTypefaceImpl(skTypeface); + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); + skTypeface = typefaceCollection.Get(typeface); + break; + } default: - return new GlyphTypefaceImpl(SKTypeface.FromFamilyName(typeface.FontFamily.Name, - (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style)); + { + skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name, + (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); + break; + } } + + return new GlyphTypefaceImpl(skTypeface); } } } diff --git a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs index dc2a40aeba..8f80d89ac6 100644 --- a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs +++ b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs @@ -95,5 +95,18 @@ namespace Avalonia.Skia.UnitTests Assert.Equal("Noto Mono", skTypeface.FamilyName); } } + + [Fact] + public void Should_Throw_For_Invalid_Custom_Font() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var fontManager = new FontManagerImpl(); + + Assert.Throws(() => + fontManager.CreateGlyphTypeface( + new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown"))); + } + } } } diff --git a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs index 1f89a5833c..627b7c2ead 100644 --- a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs @@ -332,7 +332,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - maxWidth : 200, + maxWidth : 200, maxHeight : 125, textStyleOverrides: spans); @@ -506,10 +506,30 @@ namespace Avalonia.Skia.UnitTests } } + private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。"; + + [Fact] + public void Should_Wrap() + { + using (Start()) + { + for (var i = 0; i < 2000; i++) + { + var layout = new TextLayout( + Text, + Typeface.Default, + 12, + Brushes.Black, + textWrapping: TextWrapping.Wrap, + maxWidth: 50); + } + } + } + public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), + .With(renderInterface: new PlatformRenderInterface(null), textShaperImpl: new TextShaperImpl(), fontManagerImpl : new CustomFontManagerImpl())); diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs index affdc48f5e..55656fcfc0 100644 --- a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs @@ -8,14 +8,21 @@ namespace Avalonia.UnitTests { public class MockFontManagerImpl : IFontManagerImpl { + private readonly string _defaultFamilyName; + + public MockFontManagerImpl(string defaultFamilyName = "Default") + { + _defaultFamilyName = defaultFamilyName; + } + public string GetDefaultFontFamilyName() { - return "Default"; + return _defaultFamilyName; } public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) { - return new[] { "Default" }; + return new[] { _defaultFamilyName }; } public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs index d35108080b..81a4ca6495 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs @@ -1,4 +1,5 @@ -using Avalonia.Media; +using System; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; using Xunit; @@ -19,5 +20,14 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily)); } } + + [Fact] + public void Should_Throw_When_Default_FamilyName_Is_Null() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new MockFontManagerImpl(null)))) + { + Assert.Throws(() => FontManager.Current); + } + } } } From 7e6f0b82659f529925a8a5df697a57babee048d4 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 29 Apr 2020 10:57:16 -0400 Subject: [PATCH 02/70] Add KeySpline class from WPF source --- src/Avalonia.Animation/KeySpline.cs | 271 ++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 src/Avalonia.Animation/KeySpline.cs diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs new file mode 100644 index 0000000000..34e8d89aae --- /dev/null +++ b/src/Avalonia.Animation/KeySpline.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Text; +using Avalonia; +using Avalonia.Utilities; + +// From: https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs + +namespace Avalonia.Animation +{ + [TypeConverter(typeof(KeySplineTypeConverter))] + public class KeySpline : AvaloniaObject + { + // Control points + private double _controlPointX1; + private double _controlPointY1; + private double _controlPointX2; + private double _controlPointY2; + private bool _isSpecified; + private bool _isDirty; + + // The parameter that corresponds to the most recent time + private double _parameter; + + // Cached coefficients + private double _Bx; // 3*points[0].X + private double _Cx; // 3*points[1].X + private double _Cx_Bx; // 2*(Cx - Bx) + private double _three_Cx; // 3 - Cx + + private double _By; // 3*points[0].Y + private double _Cy; // 3*points[1].Y + + // constants + private const double _accuracy = .001; // 1/3 the desired accuracy in X + private const double _fuzz = .000001; // computational zero + + public KeySpline() + { + _controlPointX1 = 0.0; + _controlPointY1 = 0.0; + _controlPointX2 = 1.0; + _controlPointY2 = 1.0; + _isDirty = true; + } + + public KeySpline(double x1, double y1, double x2, double y2) + { + _controlPointX1 = x1; + _controlPointY1 = y1; + _controlPointX2 = x2; + _controlPointY2 = y2; + _isDirty = true; + } + + public static KeySpline Parse(string value, CultureInfo culture) + { + using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline.")) + { + return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); + } + } + + public double ControlPointX1 + { + get => _controlPointX1; + set => _controlPointX1 = value; + } + + public double ControlPointY1 + { + get => _controlPointY1; + set => _controlPointY1 = value; + } + + public double ControlPointX2 + { + get => _controlPointX2; + set => _controlPointX2 = value; + } + + public double ControlPointY2 + { + get => _controlPointY2; + set => _controlPointY2 = value; + } + + /// + /// Calculates spline progress from a linear progress. + /// + /// the linear progress + /// the spline progress + public double GetSplineProgress(double linearProgress) + { + //ReadPreamble(); + + if (_isDirty) + { + Build(); + } + + if (!_isSpecified) + { + return linearProgress; + } + else + { + SetParameterFromX(linearProgress); + + return GetBezierValue(_By, _Cy, _parameter); + } + } + + /// + /// Compute cached coefficients. + /// + private void Build() + { + if (_controlPointX1 == 0 && _controlPointY1 == 0 && _controlPointX2 == 1 && _controlPointY2 == 1) + { + // This KeySpline would have no effect on the progress. + _isSpecified = false; + } + else + { + _isSpecified = true; + + _parameter = 0; + + // X coefficients + _Bx = 3 * _controlPointX1; + _Cx = 3 * _controlPointX2; + _Cx_Bx = 2 * (_Cx - _Bx); + _three_Cx = 3 - _Cx; + + // Y coefficients + _By = 3 * _controlPointY1; + _Cy = 3 * _controlPointY2; + } + + _isDirty = false; + } + + /// + /// Get an X or Y value with the Bezier formula. + /// + /// the second Bezier coefficient + /// the third Bezier coefficient + /// the parameter value to evaluate at + /// the value of the Bezier function at the given parameter + static private double GetBezierValue(double b, double c, double t) + { + double s = 1.0 - t; + double t2 = t * t; + + return b * t * s * s + c * t2 * s + t2 * t; + } + + /// + /// Get X and dX/dt at a given parameter + /// + /// the parameter value to evaluate at + /// the value of x there + /// the value of dx/dt there + private void GetXAndDx(double t, out double x, out double dx) + { + double s = 1.0 - t; + double t2 = t * t; + double s2 = s * s; + + x = _Bx * t * s2 + _Cx * t2 * s + t2 * t; + dx = _Bx * s2 + _Cx_Bx * s * t + _three_Cx * t2; + } + + /// + /// Compute the parameter value that corresponds to a given X value, using a modified + /// clamped Newton-Raphson algorithm to solve the equation X(t) - time = 0. We make + /// use of some known properties of this particular function: + /// * We are only interested in solutions in the interval [0,1] + /// * X(t) is increasing, so we can assume that if X(t) > time t > solution. We use + /// that to clamp down the search interval with every probe. + /// * The derivative of X and Y are between 0 and 3. + /// + /// the time, scaled to fit in [0,1] + private void SetParameterFromX(double time) + { + // Dynamic search interval to clamp with + double bottom = 0; + double top = 1; + + if (time == 0) + { + _parameter = 0; + } + else if (time == 1) + { + _parameter = 1; + } + else + { + // Loop while improving the guess + while (top - bottom > _fuzz) + { + double x, dx, absdx; + + // Get x and dx/dt at the current parameter + GetXAndDx(_parameter, out x, out dx); + absdx = Math.Abs(dx); + + // Clamp down the search interval, relying on the monotonicity of X(t) + if (x > time) + { + top = _parameter; // because parameter > solution + } + else + { + bottom = _parameter; // because parameter < solution + } + + // The desired accuracy is in ultimately in y, not in x, so the + // accuracy needs to be multiplied by dx/dy = (dx/dt) / (dy/dt). + // But dy/dt <=3, so we omit that + if (Math.Abs(x - time) < _accuracy * absdx) + { + break; // We're there + } + + if (absdx > _fuzz) + { + // Nonzero derivative, use Newton-Raphson to obtain the next guess + double next = _parameter - (x - time) / dx; + + // If next guess is out of the search interval then clamp it in + if (next >= top) + { + _parameter = (_parameter + top) / 2; + } + else if (next <= bottom) + { + _parameter = (_parameter + bottom) / 2; + } + else + { + // Next guess is inside the search interval, accept it + _parameter = next; + } + } + else // Zero derivative, halve the search interval + { + _parameter = (bottom + top) / 2; + } + } + } + } + } + + public class KeySplineTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return KeySpline.Parse((string)value, culture); + } + } +} From 24870351fa79ed925833b4afe11953fa0141ba29 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 29 Apr 2020 11:32:41 -0400 Subject: [PATCH 03/70] Use KeySpline in Animation --- src/Avalonia.Animation/Animation.cs | 2 +- src/Avalonia.Animation/AnimatorKeyFrame.cs | 9 +++++ .../Animators/Animator`1.cs | 3 ++ src/Avalonia.Animation/KeyFrame.cs | 20 +++++++++++ src/Avalonia.Animation/KeySpline.cs | 36 ++++++++++++++++--- 5 files changed, 65 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 1ce9f5db06..ca1d97290e 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -254,7 +254,7 @@ namespace Avalonia.Animation cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds); } - var newKF = new AnimatorKeyFrame(handler, cue); + var newKF = new AnimatorKeyFrame(handler, cue, keyframe.KeySpline); subscriptions.Add(newKF.BindSetter(setter, control)); diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 36d15e518e..f6a0c12be4 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -24,11 +24,20 @@ namespace Avalonia.Animation { AnimatorType = animatorType; Cue = cue; + KeySpline = null; + } + + public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline) + { + AnimatorType = animatorType; + Cue = cue; + KeySpline = keySpline; } internal bool isNeutral; public Type AnimatorType { get; } public Cue Cue { get; } + public KeySpline KeySpline { get; } public AvaloniaProperty Property { get; private set; } private object _value; diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index aa5e6aaf14..121ffda564 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -89,6 +89,9 @@ namespace Avalonia.Animation.Animators else newValue = (T)lastKeyframe.Value; + if (lastKeyframe.KeySpline != null) // TODO: do we use firstKeyFrame or lastKeyframe?! + progress = lastKeyframe.KeySpline.GetSplineProgress(progress); + return Interpolate(progress, oldValue, newValue); } diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index ec59586584..c2cc1aa051 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -19,6 +19,7 @@ namespace Avalonia.Animation { private TimeSpan _ktimeSpan; private Cue _kCue; + private KeySpline _kKeySpline; public KeyFrame() { @@ -74,6 +75,25 @@ namespace Avalonia.Animation } } + /// + /// Gets or sets the KeySpline of this . + /// + /// The key spline. + public KeySpline KeySpline + { + get + { + return _kKeySpline; + } + set + { + _kKeySpline = value; + if (value != null && !value.IsValid()) + { + throw new ArgumentException($"{nameof(KeySpline)} must have X coordinates >= 0.0 and <= 1.0."); + } + } + } } diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs index 34e8d89aae..dc6fb61744 100644 --- a/src/Avalonia.Animation/KeySpline.cs +++ b/src/Avalonia.Animation/KeySpline.cs @@ -66,7 +66,17 @@ namespace Avalonia.Animation public double ControlPointX1 { get => _controlPointX1; - set => _controlPointX1 = value; + set + { + if (IsValidXValue(value)) + { + _controlPointX1 = value; + } + else + { + throw new ArgumentException("Invalid KeySpline X1 value. Must be >= 0.0 and <= 1.0."); + } + } } public double ControlPointY1 @@ -78,7 +88,17 @@ namespace Avalonia.Animation public double ControlPointX2 { get => _controlPointX2; - set => _controlPointX2 = value; + set + { + if (IsValidXValue(value)) + { + _controlPointX2 = value; + } + else + { + throw new ArgumentException("Invalid KeySpline X2 value. Must be >= 0.0 and <= 1.0."); + } + } } public double ControlPointY2 @@ -94,8 +114,6 @@ namespace Avalonia.Animation /// the spline progress public double GetSplineProgress(double linearProgress) { - //ReadPreamble(); - if (_isDirty) { Build(); @@ -113,6 +131,16 @@ namespace Avalonia.Animation } } + public bool IsValid() + { + return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2); + } + + private bool IsValidXValue(double value) + { + return value >= 0.0 && value <= 1.0; + } + /// /// Compute cached coefficients. /// From 30eacab09d5023774231e219fd3edc5bfa42fb05 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 29 Apr 2020 12:10:43 -0400 Subject: [PATCH 04/70] Add some unit tests for KeySpline --- .../KeySplineTests.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/Avalonia.Animation.UnitTests/KeySplineTests.cs diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs new file mode 100644 index 0000000000..a523be2a78 --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Animation.UnitTests +{ + public class KeySplineTests + { + [Theory] + [InlineData("1,2 3,4")] + [InlineData("1 2 3 4")] + [InlineData("1 2,3 4")] + [InlineData("1,2,3,4")] + public void Can_Parse_KeySpline_Via_TypeConverter(string input) + { + var conv = new KeySplineTypeConverter(); + + var keySpline = (KeySpline)conv.ConvertFrom(input); + + Assert.Equal(1, keySpline.ControlPointX1); + Assert.Equal(2, keySpline.ControlPointY1); + Assert.Equal(3, keySpline.ControlPointX2); + Assert.Equal(4, keySpline.ControlPointY2); + } + + [Theory] + [InlineData(0.00)] + [InlineData(0.50)] + [InlineData(1.00)] + public void KeySpline_X_Values_In_Range_Do_Not_Throw(double input) + { + var keySpline = new KeySpline(); + keySpline.ControlPointX1 = input; // no exception will be thrown -- test will fail if exception thrown + keySpline.ControlPointX2 = input; // no exception will be thrown -- test will fail if exception thrown + } + + [Theory] + [InlineData(-0.01)] + [InlineData(1.01)] + public void KeySpline_X_Values_Cannot_Be_Out_Of_Range(double input) + { + var keySpline = new KeySpline(); + Assert.Throws(() => keySpline.ControlPointX1 = input); + Assert.Throws(() => keySpline.ControlPointX2 = input); + } + } +} From 47c0282ae9368d414310aa2ef167fb1e1465ab4a Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 29 Apr 2020 12:21:27 -0400 Subject: [PATCH 05/70] Update documentation --- src/Avalonia.Animation/KeySpline.cs | 49 ++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs index dc6fb61744..fc7dc8966e 100644 --- a/src/Avalonia.Animation/KeySpline.cs +++ b/src/Avalonia.Animation/KeySpline.cs @@ -6,10 +6,16 @@ using System.Text; using Avalonia; using Avalonia.Utilities; -// From: https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs +// Ported from WPF open-source code. +// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs namespace Avalonia.Animation { + /// + /// Determines how an animation is used based on a cubic bezier curve. + /// X1 and X2 must be between 0.0 and 1.0, inclusive. + /// See https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.animation.keyspline + /// [TypeConverter(typeof(KeySplineTypeConverter))] public class KeySpline : AvaloniaObject { @@ -37,6 +43,9 @@ namespace Avalonia.Animation private const double _accuracy = .001; // 1/3 the desired accuracy in X private const double _fuzz = .000001; // computational zero + /// + /// Create a with X1 = Y1 = 0 and X2 = Y2 = 1. + /// public KeySpline() { _controlPointX1 = 0.0; @@ -46,6 +55,13 @@ namespace Avalonia.Animation _isDirty = true; } + /// + /// Create a with the given parameters + /// + /// X coordinate for the first control point + /// Y coordinate for the first control point + /// X coordinate for the second control point + /// Y coordinate for the second control point public KeySpline(double x1, double y1, double x2, double y2) { _controlPointX1 = x1; @@ -55,6 +71,14 @@ namespace Avalonia.Animation _isDirty = true; } + /// + /// Parse a from a string. The string + /// needs to contain 4 values in it for the 2 control points. + /// + /// string with 4 values in it + /// culture of the string + /// Thrown if the string does not have 4 values + /// A with the appropriate values set public static KeySpline Parse(string value, CultureInfo culture) { using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline.")) @@ -63,6 +87,9 @@ namespace Avalonia.Animation } } + /// + /// X coordinate of the first control point + /// public double ControlPointX1 { get => _controlPointX1; @@ -79,12 +106,18 @@ namespace Avalonia.Animation } } + /// + /// Y coordinate of the first control point + /// public double ControlPointY1 { get => _controlPointY1; set => _controlPointY1 = value; } + /// + /// X coordinate of the second control point + /// public double ControlPointX2 { get => _controlPointX2; @@ -101,6 +134,9 @@ namespace Avalonia.Animation } } + /// + /// Y coordinate of the second control point + /// public double ControlPointY2 { get => _controlPointY2; @@ -131,11 +167,22 @@ namespace Avalonia.Animation } } + /// + /// Check to see whether the is valid by looking + /// at its X values. + /// + /// true if the X values for this fall in + /// acceptable range; false otherwise. public bool IsValid() { return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2); } + /// + /// + /// + /// + /// private bool IsValidXValue(double value) { return value >= 0.0 && value <= 1.0; From 4cf4bb953bd7a11dfb80718e61730fec627b97a8 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 29 Apr 2020 12:39:25 -0400 Subject: [PATCH 06/70] Update docs again (missed KeySplineTypeConverter) --- src/Avalonia.Animation/KeySpline.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs index fc7dc8966e..5a4f7a15a3 100644 --- a/src/Avalonia.Animation/KeySpline.cs +++ b/src/Avalonia.Animation/KeySpline.cs @@ -331,6 +331,9 @@ namespace Avalonia.Animation } } + /// + /// Converts string values to values + /// public class KeySplineTypeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) From 7d80492a1268ec021bd66a012e67d62738378025 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 29 Apr 2020 22:25:11 -0300 Subject: [PATCH 07/70] Add Fullscreen to WindowState enum. --- src/Avalonia.Controls/WindowState.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls/WindowState.cs b/src/Avalonia.Controls/WindowState.cs index 4ed30e726e..777b52dc11 100644 --- a/src/Avalonia.Controls/WindowState.cs +++ b/src/Avalonia.Controls/WindowState.cs @@ -19,5 +19,10 @@ namespace Avalonia.Controls /// The window is maximized. /// Maximized, + + /// + /// The window is fullscreen. + /// + FullScreen, } } From 02fa37772871df8958dc89829fc4d78688a35afe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 29 Apr 2020 22:26:08 -0300 Subject: [PATCH 08/70] add missing window style. --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index cbfa1abfb7..5c364c4038 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -460,6 +460,7 @@ namespace Avalonia.Win32.Interop WS_SIZEFRAME = 0x40000, WS_SYSMENU = 0x80000, WS_TABSTOP = 0x10000, + WS_THICKFRAME = 0x40000, WS_VISIBLE = 0x10000000, WS_VSCROLL = 0x200000, WS_EX_DLGMODALFRAME = 0x00000001, From dd776df3c7a52b38e5f8bfefecc36cbdf1d54e36 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 29 Apr 2020 22:27:03 -0300 Subject: [PATCH 09/70] add extension method to convert between RECT and PixelRect. --- src/Windows/Avalonia.Win32/Win32TypeExtensions.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/Windows/Avalonia.Win32/Win32TypeExtensions.cs diff --git a/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs b/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs new file mode 100644 index 0000000000..0e9daf3137 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs @@ -0,0 +1,13 @@ +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32 +{ + public static class Win32TypeExtensions + { + internal static PixelRect ToPixelRect(this RECT rect) + { + return new PixelRect(rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top); + } + } +} From 05a2a648712436f3b4c514938fd50323502e0698 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 29 Apr 2020 22:29:06 -0300 Subject: [PATCH 10/70] use extension method in screens api. --- src/Windows/Avalonia.Win32/ScreenImpl.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs index 963042b249..442794f0f0 100644 --- a/src/Windows/Avalonia.Win32/ScreenImpl.cs +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -8,7 +8,7 @@ namespace Avalonia.Win32 { public class ScreenImpl : IScreenImpl { - public int ScreenCount + public int ScreenCount { get => GetSystemMetrics(SystemMetric.SM_CMONITORS); } @@ -33,7 +33,7 @@ namespace Avalonia.Win32 var shcore = LoadLibrary("shcore.dll"); var method = GetProcAddress(shcore, nameof(GetDpiForMonitor)); if (method != IntPtr.Zero) - { + { GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _); dpi = (double)x; } @@ -51,11 +51,8 @@ namespace Avalonia.Win32 RECT bounds = monitorInfo.rcMonitor; RECT workingArea = monitorInfo.rcWork; - PixelRect avaloniaBounds = new PixelRect(bounds.left, bounds.top, bounds.right - bounds.left, - bounds.bottom - bounds.top); - PixelRect avaloniaWorkArea = - new PixelRect(workingArea.left, workingArea.top, workingArea.right - workingArea.left, - workingArea.bottom - workingArea.top); + PixelRect avaloniaBounds = bounds.ToPixelRect(); + PixelRect avaloniaWorkArea = workingArea.ToPixelRect(); screens[index] = new WinScreen(dpi / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, monitor); From 5eee1e04e5d9e009d20139277bedefa735a52a17 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 29 Apr 2020 22:35:37 -0300 Subject: [PATCH 11/70] win32 implementation for fullscreen windowstate. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 118 ++++++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e193c72ef7..5f88fc3f0c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -37,10 +37,21 @@ namespace Avalonia.Win32 { WindowEdge.West, HitTestValues.HTLEFT } }; + private struct SavedWindowInfo + { + public WindowStyles Style { get; set; } + public WindowStyles ExStyle { get; set; } + public RECT WindowRect { get; set; } + }; + + private SavedWindowInfo _savedWindowInfo; + private bool _fullScreen; + #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; #endif + private readonly List _disabledBy; private readonly TouchDevice _touchDevice; private readonly MouseDevice _mouseDevice; @@ -82,7 +93,9 @@ namespace Avalonia.Win32 _windowProperties = new WindowProperties { - ShowInTaskbar = false, IsResizable = true, Decorations = SystemDecorations.Full + ShowInTaskbar = false, + IsResizable = true, + Decorations = SystemDecorations.Full }; _rendererLock = new ManagedDeferredRendererLock(); @@ -205,6 +218,90 @@ namespace Avalonia.Win32 } } + private void MarkFullscreen(bool fullscreen) + { + //if (!task_bar_list_) + //{ + // HRESULT hr = + + + // ::CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, + // IID_PPV_ARGS(&task_bar_list_)); + // if (SUCCEEDED(hr) && FAILED(task_bar_list_->HrInit())) + // task_bar_list_ = nullptr; + //} + + //// As per MSDN marking the window as fullscreen should ensure that the + //// taskbar is moved to the bottom of the Z-order when the fullscreen window + //// is activated. If the window is not fullscreen, the Shell falls back to + //// heuristics to determine how the window should be treated, which means + //// that it could still consider the window as fullscreen. :( + //if (task_bar_list_) + // task_bar_list_->MarkFullscreenWindow(hwnd_, !!fullscreen); + } + + private void SetFullScreen(bool fullscreen) + { + // Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + //std::unique_ptr visibility; + + // With Aero enabled disabling the visibility causes the window to disappear + // for several frames, which looks worse than doing other updates + // non-atomically. + //if (!ui::win::IsAeroGlassEnabled()) + // visibility = std::make_unique(hwnd_); + + // Save current window state if not already fullscreen. + if (!_fullScreen) + { + _savedWindowInfo.Style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); + _savedWindowInfo.ExStyle = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE); + GetWindowRect(_hwnd, out var windowRect); + _savedWindowInfo.WindowRect = windowRect; + } + + _fullScreen = fullscreen; + + if (_fullScreen) + { + // Set new window style and size. + SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, + (uint)(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME))); + SetWindowLong( + _hwnd, (int)WindowLongParam.GWL_EXSTYLE, + (uint)(_savedWindowInfo.ExStyle & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | + WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE))); + + // On expand, if we're given a window_rect, grow to it, otherwise do + // not resize. + MONITORINFO monitor_info = MONITORINFO.Create(); + GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info); + + var window_rect = monitor_info.rcMonitor.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y, + window_rect.Width, window_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + } + else + { + // Reset original window style and size. The multiple window size/moves + // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be + // repainted. Better-looking methods welcome. + SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)_savedWindowInfo.Style); + SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)_savedWindowInfo.ExStyle); + + // On restore, resize to the previous saved rect size. + var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, + new_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + } + + MarkFullscreen(fullscreen); + } + public IEnumerable Surfaces => new object[] { Handle, _gl, _framebuffer }; public PixelPoint Position @@ -545,16 +642,35 @@ namespace Avalonia.Win32 switch (state) { case WindowState.Minimized: + if (_fullScreen) + { + SetFullScreen(false); + } + command = ShowWindowCommand.Minimize; break; case WindowState.Maximized: + if (_fullScreen) + { + SetFullScreen(false); + } + command = ShowWindowCommand.Maximize; break; case WindowState.Normal: + if (_fullScreen) + { + SetFullScreen(false); + } + command = ShowWindowCommand.Restore; break; + case WindowState.FullScreen: + SetFullScreen(true); + return; + default: throw new ArgumentException("Invalid WindowState."); } From 996821b60b8b60159d911b9ee6499db262bfc4fb Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Thu, 30 Apr 2020 09:06:50 -0400 Subject: [PATCH 12/70] Removed TODO now that behavior is verified with WPF --- src/Avalonia.Animation/Animators/Animator`1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index 121ffda564..0660440e30 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -89,7 +89,7 @@ namespace Avalonia.Animation.Animators else newValue = (T)lastKeyframe.Value; - if (lastKeyframe.KeySpline != null) // TODO: do we use firstKeyFrame or lastKeyframe?! + if (lastKeyframe.KeySpline != null) progress = lastKeyframe.KeySpline.GetSplineProgress(progress); return Interpolate(progress, oldValue, newValue); From dc75bf512a8122cb6c2b168231979e6584da2b4a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 16:42:25 +0200 Subject: [PATCH 13/70] Run sizing tests with both Show and ShowDialog. One test now failing with `ShowDialog` due to #3843. --- .../WindowTests.cs | 313 +++++++++--------- 1 file changed, 165 insertions(+), 148 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 29944c6f85..e99be9cfd2 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -336,210 +336,227 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual() + public class SizingTests { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual() { - var child = new ChildControl(); - var target = new Window - { - Width = 100, - Height = 50, - SizeToContent = SizeToContent.Manual, - Content = child - }; + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new ChildControl(); + var target = new Window + { + Width = 100, + Height = 50, + SizeToContent = SizeToContent.Manual, + Content = child + }; - target.Show(); + Show(target); - Assert.Equal(1, child.MeasureSizes.Count); - Assert.Equal(new Size(100, 50), child.MeasureSizes[0]); + Assert.Equal(1, child.MeasureSizes.Count); + Assert.Equal(new Size(100, 50), child.MeasureSizes[0]); + } } - } - [Fact] - public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified() { - var windowImpl = MockWindowingPlatform.CreateWindowMock(); - windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450)); - - var child = new ChildControl(); - var target = new Window(windowImpl.Object) + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - SizeToContent = SizeToContent.Manual, - Content = child - }; + var windowImpl = MockWindowingPlatform.CreateWindowMock(); + windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450)); - target.Show(); + var child = new ChildControl(); + var target = new Window(windowImpl.Object) + { + SizeToContent = SizeToContent.Manual, + Content = child + }; - Assert.Equal(1, child.MeasureSizes.Count); - Assert.Equal(new Size(550, 450), child.MeasureSizes[0]); + Show(target); + + Assert.Equal(1, child.MeasureSizes.Count); + Assert.Equal(new Size(550, 450), child.MeasureSizes[0]); + } } - } - [Fact] - public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight() { - var child = new ChildControl(); - var target = new Window + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Width = 100, - Height = 50, - SizeToContent = SizeToContent.WidthAndHeight, - Content = child - }; + var child = new ChildControl(); + var target = new Window + { + Width = 100, + Height = 50, + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; - target.Show(); + Show(target); - Assert.Equal(1, child.MeasureSizes.Count); - Assert.Equal(Size.Infinity, child.MeasureSizes[0]); + Assert.Equal(1, child.MeasureSizes.Count); + Assert.Equal(Size.Infinity, child.MeasureSizes[0]); + } } - } - [Fact] - public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size() - { - // Issue #3784. - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size() { - var windowImpl = MockWindowingPlatform.CreateWindowMock(); - var clientSize = new Size(200, 200); - var maxClientSize = new Size(480, 480); - - windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(size => + // Issue #3784. + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - clientSize = size.Constrain(maxClientSize); - windowImpl.Object.Resized?.Invoke(clientSize); - }); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); + var clientSize = new Size(200, 200); + var maxClientSize = new Size(480, 480); - windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); + windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(size => + { + clientSize = size.Constrain(maxClientSize); + windowImpl.Object.Resized?.Invoke(clientSize); + }); - var child = new Canvas - { - Width = 400, - Height = 800, - }; - var target = new Window(windowImpl.Object) - { - SizeToContent = SizeToContent.WidthAndHeight, - Content = child - }; + windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); - target.Show(); + var child = new Canvas + { + Width = 400, + Height = 800, + }; + var target = new Window(windowImpl.Object) + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; + + Show(target); - Assert.Equal(new Size(400, 480), target.Bounds.Size); + Assert.Equal(new Size(400, 480), target.Bounds.Size); - // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no - // parent control to be offset against. - Assert.Equal(new Point(0, 0), target.Bounds.Position); + // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no + // parent control to be offset against. + Assert.Equal(new Point(0, 0), target.Bounds.Position); + } } - } - [Fact] - public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight() { - var child = new Canvas + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Width = 400, - Height = 800, - }; + var child = new Canvas + { + Width = 400, + Height = 800, + }; - var target = new Window() - { - SizeToContent = SizeToContent.WidthAndHeight, - Content = child - }; + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; - target.Show(); + Show(target); - Assert.Equal(400, target.Width); - Assert.Equal(800, target.Height); + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + } } - } - [Fact] - public void SizeToContent_Should_Not_Be_Lost_On_Show() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void SizeToContent_Should_Not_Be_Lost_On_Show() { - var child = new Canvas + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Width = 400, - Height = 800, - }; + var child = new Canvas + { + Width = 400, + Height = 800, + }; - var target = new Window() - { - SizeToContent = SizeToContent.WidthAndHeight, - Content = child - }; + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; - target.Show(); + Show(target); - Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); + Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); + } } - } - [Fact] - public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight() { - var child = new Canvas + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Width = 400, - Height = 800, - }; + var child = new Canvas + { + Width = 400, + Height = 800, + }; - var target = new Window() - { - SizeToContent = SizeToContent.WidthAndHeight, - Content = child - }; + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; - target.Show(); + Show(target); - Assert.Equal(400, target.Width); - Assert.Equal(800, target.Height); + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); - child.Width = 410; - target.LayoutManager.ExecuteLayoutPass(); + child.Width = 410; + target.LayoutManager.ExecuteLayoutPass(); - Assert.Equal(410, target.Width); - Assert.Equal(800, target.Height); - Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); + Assert.Equal(410, target.Width); + Assert.Equal(800, target.Height); + Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); + } } - } - [Fact] - public void Setting_Width_Should_Resize_WindowImpl() - { - // Issue #3796 - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Setting_Width_Should_Resize_WindowImpl() { - var target = new Window() + // Issue #3796 + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Width = 400, - Height = 800, - }; + var target = new Window() + { + Width = 400, + Height = 800, + }; - target.Show(); + Show(target); + + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); - Assert.Equal(400, target.Width); - Assert.Equal(800, target.Height); + target.Width = 410; + target.LayoutManager.ExecuteLayoutPass(); - target.Width = 410; - target.LayoutManager.ExecuteLayoutPass(); + var windowImpl = Mock.Get(target.PlatformImpl); + windowImpl.Verify(x => x.Resize(new Size(410, 800))); + Assert.Equal(410, target.Width); + } + } - var windowImpl = Mock.Get(target.PlatformImpl); - windowImpl.Verify(x => x.Resize(new Size(410, 800))); - Assert.Equal(410, target.Width); + protected virtual void Show(Window window) + { + window.Show(); + } + } + + public class DialogSizingTests : SizingTests + { + protected override void Show(Window window) + { + var owner = new Window(); + window.ShowDialog(owner); } } From 4ee9700f08d61625ebc1615129b27d462c3e053a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 16:45:53 +0200 Subject: [PATCH 14/70] Set initial size when showing Window as dialog. Fixes #3843. --- src/Avalonia.Controls/Window.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index dd00b850fe..75f32c862e 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -500,6 +500,19 @@ namespace Avalonia.Controls EnsureInitialized(); IsVisible = true; + + var initialSize = new Size( + double.IsNaN(Width) ? ClientSize.Width : Width, + double.IsNaN(Height) ? ClientSize.Height : Height); + + if (initialSize != ClientSize) + { + using (BeginAutoSizing()) + { + PlatformImpl?.Resize(initialSize); + } + } + LayoutManager.ExecuteInitialLayoutPass(this); var result = new TaskCompletionSource(); From d1e4614a3855e1137a8dc6326c54570bee8b689b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 17:13:18 +0200 Subject: [PATCH 15/70] Added ComboBox.PopupClosedOverride. To control whether the event that caused the popup to close is swallowed or passed through. For now just implemented on `ComboBox` - we may want to implement it in future on all controls that display popups if this is something that proves generally useful. Fixes #3845. --- src/Avalonia.Controls/ComboBox.cs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 1daa6a5630..67ef6cd1e9 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -234,6 +234,23 @@ namespace Avalonia.Controls base.OnTemplateApplied(e); } + /// + /// Called when the ComboBox popup is closed, with the + /// that caused the popup to close. + /// + /// The event args. + /// + /// This method can be overridden to control whether the event that caused the popup to close + /// is swallowed or passed through. + /// + protected virtual void PopupClosedOverride(PopupClosedEventArgs e) + { + if (e.CloseEvent is PointerEventArgs pointerEvent) + { + pointerEvent.Handled = true; + } + } + internal void ItemFocused(ComboBoxItem dropDownItem) { if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) @@ -247,10 +264,7 @@ namespace Avalonia.Controls _subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen = null; - if (e.CloseEvent is PointerEventArgs pointerEvent) - { - pointerEvent.Handled = true; - } + PopupClosedOverride(e); if (CanFocus(this)) { From 26ad865589f7b8ac83d0b34fc5e209e023a50302 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:39:56 -0300 Subject: [PATCH 16/70] Add Guids for TaskBarList interop. --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 5c364c4038..e9f00a0c7f 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1643,6 +1643,8 @@ namespace Avalonia.Win32.Interop public static readonly Guid SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B"); public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); public static readonly Guid IShellItem = Guid.Parse("43826D1E-E718-42EE-BC55-A1E261C37BFE"); + public static readonly Guid TaskBarList = Guid.Parse("56FDF344-FD6D-11D0-958A-006097C9A090"); + public static readonly Guid ITaskBarList2 = Guid.Parse("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"); } [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] From 1ce8f4f6bad28ac2e523e6b6a097bd040cbbbb6c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:40:45 -0300 Subject: [PATCH 17/70] use helper methods GetStyle / SetStyle. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 5f88fc3f0c..6bbcb3dc92 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -254,8 +254,8 @@ namespace Avalonia.Win32 // Save current window state if not already fullscreen. if (!_fullScreen) { - _savedWindowInfo.Style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); - _savedWindowInfo.ExStyle = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE); + _savedWindowInfo.Style = GetStyle(); + _savedWindowInfo.ExStyle = GetExtendedStyle(); GetWindowRect(_hwnd, out var windowRect); _savedWindowInfo.WindowRect = windowRect; } @@ -265,12 +265,8 @@ namespace Avalonia.Win32 if (_fullScreen) { // Set new window style and size. - SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, - (uint)(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME))); - SetWindowLong( - _hwnd, (int)WindowLongParam.GWL_EXSTYLE, - (uint)(_savedWindowInfo.ExStyle & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | - WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE))); + SetStyle(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME)); + SetExtendedStyle(_savedWindowInfo.ExStyle & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE)); // On expand, if we're given a window_rect, grow to it, otherwise do // not resize. @@ -288,8 +284,8 @@ namespace Avalonia.Win32 // Reset original window style and size. The multiple window size/moves // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be // repainted. Better-looking methods welcome. - SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)_savedWindowInfo.Style); - SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)_savedWindowInfo.ExStyle); + SetStyle(_savedWindowInfo.Style); + SetExtendedStyle(_savedWindowInfo.ExStyle); // On restore, resize to the previous saved rect size. var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); From ecd4cd7bbf4a8eeecd3815c772fddda6fdbb3dc5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:43:11 -0300 Subject: [PATCH 18/70] Add CoCreateInstance overload that gives intptr. --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index e9f00a0c7f..43beb35e45 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1147,7 +1147,10 @@ namespace Avalonia.Win32.Interop internal static extern int CoCreateInstance(ref Guid clsid, IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter); - + [DllImport("ole32.dll", PreserveSig = true)] + internal static extern int CoCreateInstance(ref Guid clsid, + IntPtr ignore1, int ignore2, ref Guid iid, [Out] out IntPtr pUnkOuter); + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv); From 93e43951dbb58cacca68d78a1ddfade135de250f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:43:43 -0300 Subject: [PATCH 19/70] Add delegates and VTables for ITaskBarList2. --- .../Avalonia.Win32/ITaskBarList2VTable.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Windows/Avalonia.Win32/ITaskBarList2VTable.cs diff --git a/src/Windows/Avalonia.Win32/ITaskBarList2VTable.cs b/src/Windows/Avalonia.Win32/ITaskBarList2VTable.cs new file mode 100644 index 0000000000..c187251ee3 --- /dev/null +++ b/src/Windows/Avalonia.Win32/ITaskBarList2VTable.cs @@ -0,0 +1,22 @@ +using System; +using System.Runtime.InteropServices; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32 +{ + delegate void MarkFullscreenWindow(IntPtr This, IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fullscreen); + delegate HRESULT HrInit(IntPtr This); + + struct ITaskBarList2VTable + { + public IntPtr IUnknown1; + public IntPtr IUnknown2; + public IntPtr IUnknown3; + public IntPtr HrInit; + public IntPtr AddTab; + public IntPtr DeleteTab; + public IntPtr ActivateTab; + public IntPtr SetActiveAlt; + public IntPtr MarkFullscreenWindow; + } +} From cecce99c954be7b7cccd467403ae58500ae171d8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:44:11 -0300 Subject: [PATCH 20/70] SystemDialogs call the correct cocreateinstance overload. --- src/Windows/Avalonia.Win32/SystemDialogImpl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index 8bdd4b7bfa..c6164e0868 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -24,7 +24,7 @@ namespace Avalonia.Win32 Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; Guid iid = UnmanagedMethods.ShellIds.IFileDialog; - UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk); var frm = (UnmanagedMethods.IFileDialog)unk; var openDialog = dialog as OpenFileDialog; @@ -105,9 +105,9 @@ namespace Avalonia.Win32 var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; - Guid iid = UnmanagedMethods.ShellIds.IFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; - UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk); var frm = (UnmanagedMethods.IFileDialog)unk; uint options; frm.GetOptions(out options); From 90f386ffc0468df5e2678a777acb80e6e7b1faf8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:44:37 -0300 Subject: [PATCH 21/70] correctly port MarkFullScreen using CoreRT compatible COM. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 58 ++++++++++++++---------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 6bbcb3dc92..ba7f90eb63 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -46,6 +46,7 @@ namespace Avalonia.Win32 private SavedWindowInfo _savedWindowInfo; private bool _fullScreen; + private IntPtr _taskBarList; #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; @@ -218,39 +219,46 @@ namespace Avalonia.Win32 } } - private void MarkFullscreen(bool fullscreen) + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// + /// Fullscreen state. + private unsafe void MarkFullscreen(bool fullscreen) { - //if (!task_bar_list_) - //{ - // HRESULT hr = + if (_taskBarList == IntPtr.Zero) + { + Guid clsid = ShellIds.TaskBarList; + Guid iid = ShellIds.ITaskBarList2; + + int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out _taskBarList); + + if (_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)_taskBarList.ToPointer(); + var hrInit = Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); - // ::CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, - // IID_PPV_ARGS(&task_bar_list_)); - // if (SUCCEEDED(hr) && FAILED(task_bar_list_->HrInit())) - // task_bar_list_ = nullptr; - //} + if (hrInit(_taskBarList) != HRESULT.S_OK) + { + _taskBarList = IntPtr.Zero; + } + } + } - //// As per MSDN marking the window as fullscreen should ensure that the - //// taskbar is moved to the bottom of the Z-order when the fullscreen window - //// is activated. If the window is not fullscreen, the Shell falls back to - //// heuristics to determine how the window should be treated, which means - //// that it could still consider the window as fullscreen. :( - //if (task_bar_list_) - // task_bar_list_->MarkFullscreenWindow(hwnd_, !!fullscreen); + if (_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)_taskBarList.ToPointer(); + var markFullscreen = Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); + markFullscreen(_taskBarList, _hwnd, fullscreen); + } } + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// + /// private void SetFullScreen(bool fullscreen) { - // Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc - //std::unique_ptr visibility; - - // With Aero enabled disabling the visibility causes the window to disappear - // for several frames, which looks worse than doing other updates - // non-atomically. - //if (!ui::win::IsAeroGlassEnabled()) - // visibility = std::make_unique(hwnd_); - // Save current window state if not already fullscreen. if (!_fullScreen) { From 81c4a1e796c6814edfde812fa1ff34464186ac2e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:46:32 -0300 Subject: [PATCH 22/70] move struct. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ba7f90eb63..f3538a9bfe 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -37,13 +37,6 @@ namespace Avalonia.Win32 { WindowEdge.West, HitTestValues.HTLEFT } }; - private struct SavedWindowInfo - { - public WindowStyles Style { get; set; } - public WindowStyles ExStyle { get; set; } - public RECT WindowRect { get; set; } - }; - private SavedWindowInfo _savedWindowInfo; private bool _fullScreen; private IntPtr _taskBarList; @@ -833,6 +826,13 @@ namespace Avalonia.Win32 IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + private struct SavedWindowInfo + { + public WindowStyles Style { get; set; } + public WindowStyles ExStyle { get; set; } + public RECT WindowRect { get; set; } + }; + private struct WindowProperties { public bool ShowInTaskbar; From 51ad905a2c3e73e832178222a5586a194add9e1b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:48:42 -0300 Subject: [PATCH 23/70] use WindowProperties to hold fullscreen flag. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f3538a9bfe..9553447623 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -38,7 +38,6 @@ namespace Avalonia.Win32 }; private SavedWindowInfo _savedWindowInfo; - private bool _fullScreen; private IntPtr _taskBarList; #if USE_MANAGED_DRAG @@ -253,7 +252,7 @@ namespace Avalonia.Win32 private void SetFullScreen(bool fullscreen) { // Save current window state if not already fullscreen. - if (!_fullScreen) + if (!_windowProperties.IsFullScreen) { _savedWindowInfo.Style = GetStyle(); _savedWindowInfo.ExStyle = GetExtendedStyle(); @@ -261,9 +260,9 @@ namespace Avalonia.Win32 _savedWindowInfo.WindowRect = windowRect; } - _fullScreen = fullscreen; + _windowProperties.IsFullScreen = fullscreen; - if (_fullScreen) + if (_windowProperties.IsFullScreen) { // Set new window style and size. SetStyle(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME)); @@ -639,7 +638,7 @@ namespace Avalonia.Win32 switch (state) { case WindowState.Minimized: - if (_fullScreen) + if (_windowProperties.IsFullScreen) { SetFullScreen(false); } @@ -647,7 +646,7 @@ namespace Avalonia.Win32 command = ShowWindowCommand.Minimize; break; case WindowState.Maximized: - if (_fullScreen) + if (_windowProperties.IsFullScreen) { SetFullScreen(false); } @@ -656,7 +655,7 @@ namespace Avalonia.Win32 break; case WindowState.Normal: - if (_fullScreen) + if (_windowProperties.IsFullScreen) { SetFullScreen(false); } @@ -838,6 +837,7 @@ namespace Avalonia.Win32 public bool ShowInTaskbar; public bool IsResizable; public SystemDecorations Decorations; + public bool IsFullScreen; } } } From fe954a0398baf00fd60277841847480f379d4850 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:51:01 -0300 Subject: [PATCH 24/70] move private methods. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 174 +++++++++++------------ 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9553447623..842bf3dc72 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -211,93 +211,6 @@ namespace Avalonia.Win32 } } - /// - /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc - /// - /// Fullscreen state. - private unsafe void MarkFullscreen(bool fullscreen) - { - if (_taskBarList == IntPtr.Zero) - { - Guid clsid = ShellIds.TaskBarList; - Guid iid = ShellIds.ITaskBarList2; - - int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out _taskBarList); - - if (_taskBarList != IntPtr.Zero) - { - var ptr = (ITaskBarList2VTable**)_taskBarList.ToPointer(); - - var hrInit = Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); - - if (hrInit(_taskBarList) != HRESULT.S_OK) - { - _taskBarList = IntPtr.Zero; - } - } - } - - if (_taskBarList != IntPtr.Zero) - { - var ptr = (ITaskBarList2VTable**)_taskBarList.ToPointer(); - var markFullscreen = Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); - markFullscreen(_taskBarList, _hwnd, fullscreen); - } - } - - /// - /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc - /// - /// - private void SetFullScreen(bool fullscreen) - { - // Save current window state if not already fullscreen. - if (!_windowProperties.IsFullScreen) - { - _savedWindowInfo.Style = GetStyle(); - _savedWindowInfo.ExStyle = GetExtendedStyle(); - GetWindowRect(_hwnd, out var windowRect); - _savedWindowInfo.WindowRect = windowRect; - } - - _windowProperties.IsFullScreen = fullscreen; - - if (_windowProperties.IsFullScreen) - { - // Set new window style and size. - SetStyle(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME)); - SetExtendedStyle(_savedWindowInfo.ExStyle & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE)); - - // On expand, if we're given a window_rect, grow to it, otherwise do - // not resize. - MONITORINFO monitor_info = MONITORINFO.Create(); - GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info); - - var window_rect = monitor_info.rcMonitor.ToPixelRect(); - - SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y, - window_rect.Width, window_rect.Height, - SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); - } - else - { - // Reset original window style and size. The multiple window size/moves - // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be - // repainted. Better-looking methods welcome. - SetStyle(_savedWindowInfo.Style); - SetExtendedStyle(_savedWindowInfo.ExStyle); - - // On restore, resize to the previous saved rect size. - var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); - - SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, - new_rect.Height, - SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); - } - - MarkFullscreen(fullscreen); - } - public IEnumerable Surfaces => new object[] { Handle, _gl, _framebuffer }; public PixelPoint Position @@ -631,6 +544,93 @@ namespace Avalonia.Win32 } } + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// + /// Fullscreen state. + private unsafe void MarkFullscreen(bool fullscreen) + { + if (_taskBarList == IntPtr.Zero) + { + Guid clsid = ShellIds.TaskBarList; + Guid iid = ShellIds.ITaskBarList2; + + int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out _taskBarList); + + if (_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)_taskBarList.ToPointer(); + + var hrInit = Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); + + if (hrInit(_taskBarList) != HRESULT.S_OK) + { + _taskBarList = IntPtr.Zero; + } + } + } + + if (_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)_taskBarList.ToPointer(); + var markFullscreen = Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); + markFullscreen(_taskBarList, _hwnd, fullscreen); + } + } + + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// + /// + private void SetFullScreen(bool fullscreen) + { + // Save current window state if not already fullscreen. + if (!_windowProperties.IsFullScreen) + { + _savedWindowInfo.Style = GetStyle(); + _savedWindowInfo.ExStyle = GetExtendedStyle(); + GetWindowRect(_hwnd, out var windowRect); + _savedWindowInfo.WindowRect = windowRect; + } + + _windowProperties.IsFullScreen = fullscreen; + + if (_windowProperties.IsFullScreen) + { + // Set new window style and size. + SetStyle(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME)); + SetExtendedStyle(_savedWindowInfo.ExStyle & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE)); + + // On expand, if we're given a window_rect, grow to it, otherwise do + // not resize. + MONITORINFO monitor_info = MONITORINFO.Create(); + GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info); + + var window_rect = monitor_info.rcMonitor.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y, + window_rect.Width, window_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + } + else + { + // Reset original window style and size. The multiple window size/moves + // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be + // repainted. Better-looking methods welcome. + SetStyle(_savedWindowInfo.Style); + SetExtendedStyle(_savedWindowInfo.ExStyle); + + // On restore, resize to the previous saved rect size. + var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, + new_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + } + + MarkFullscreen(fullscreen); + } + private void ShowWindow(WindowState state) { ShowWindowCommand command; From a41162dce6fd27794cde937c57c0785f2d5d0d38 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 14:05:06 -0300 Subject: [PATCH 25/70] use UpdateWindowProperties to MarkFullScreen. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 47 ++++++++++++------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 842bf3dc72..c2f8eb3dbf 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -580,23 +580,19 @@ namespace Avalonia.Win32 /// /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// Method must only be called from inside UpdateWindowProperties. /// /// private void SetFullScreen(bool fullscreen) { - // Save current window state if not already fullscreen. - if (!_windowProperties.IsFullScreen) + if (fullscreen) { + // Save current window state if not already fullscreen. _savedWindowInfo.Style = GetStyle(); _savedWindowInfo.ExStyle = GetExtendedStyle(); GetWindowRect(_hwnd, out var windowRect); _savedWindowInfo.WindowRect = windowRect; - } - - _windowProperties.IsFullScreen = fullscreen; - if (_windowProperties.IsFullScreen) - { // Set new window style and size. SetStyle(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME)); SetExtendedStyle(_savedWindowInfo.ExStyle & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE)); @@ -635,42 +631,40 @@ namespace Avalonia.Win32 { ShowWindowCommand command; + var newWindowProperties = _windowProperties; + switch (state) { case WindowState.Minimized: - if (_windowProperties.IsFullScreen) - { - SetFullScreen(false); - } - + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Minimize; break; case WindowState.Maximized: - if (_windowProperties.IsFullScreen) - { - SetFullScreen(false); - } - + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Maximize; break; case WindowState.Normal: - if (_windowProperties.IsFullScreen) - { - SetFullScreen(false); - } - + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Restore; break; case WindowState.FullScreen: - SetFullScreen(true); - return; + newWindowProperties.IsFullScreen = true; + command = ShowWindowCommand.Maximize; + break; default: throw new ArgumentException("Invalid WindowState."); } + UpdateWindowProperties(newWindowProperties); + + if (newWindowProperties.IsFullScreen) + { + return; + } + UnmanagedMethods.ShowWindow(_hwnd, command); if (state == WindowState.Maximized) @@ -800,6 +794,11 @@ namespace Avalonia.Win32 SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); } + + if (oldProperties.IsFullScreen != newProperties.IsFullScreen) + { + SetFullScreen(newProperties.IsFullScreen); + } } #if USE_MANAGED_DRAG From bfa96382f7b7ac617c1f55b14463c8f30a3bc9d2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 14:06:12 -0300 Subject: [PATCH 26/70] cleaner code. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index c2f8eb3dbf..97ec32cfb5 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -651,8 +651,8 @@ namespace Avalonia.Win32 case WindowState.FullScreen: newWindowProperties.IsFullScreen = true; - command = ShowWindowCommand.Maximize; - break; + UpdateWindowProperties(newWindowProperties); + return; default: throw new ArgumentException("Invalid WindowState."); @@ -660,11 +660,6 @@ namespace Avalonia.Win32 UpdateWindowProperties(newWindowProperties); - if (newWindowProperties.IsFullScreen) - { - return; - } - UnmanagedMethods.ShowWindow(_hwnd, command); if (state == WindowState.Maximized) From 62b8da3cdf2dd4741078b7b82af972b95185e0ca Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Thu, 30 Apr 2020 15:46:07 -0400 Subject: [PATCH 27/70] Added a test for KeySpline animation --- .../KeySplineTests.cs | 76 ++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs index a523be2a78..b98564a03c 100644 --- a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs +++ b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs @@ -1,6 +1,7 @@ using System; -using Avalonia.Controls; -using Avalonia.UnitTests; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Styling; using Xunit; namespace Avalonia.Animation.UnitTests @@ -44,5 +45,76 @@ namespace Avalonia.Animation.UnitTests Assert.Throws(() => keySpline.ControlPointX1 = input); Assert.Throws(() => keySpline.ControlPointX2 = input); } + + [Fact] + public void Check_KeySpline_Handled_properly() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(RotateTransform.AngleProperty, -2.5d), + }, + KeyTime = TimeSpan.FromSeconds(0) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(RotateTransform.AngleProperty, 2.5d), + }, + KeyTime = TimeSpan.FromSeconds(5), + KeySpline = new KeySpline(0.1123555056179775, + 0.657303370786517, + 0.8370786516853934, + 0.499999999999999999) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(5), + Children = + { + keyframe1, + keyframe2 + }, + IterationCount = new IterationCount(5), + PlaybackDirection = PlaybackDirection.Alternate + }; + + var rotateTransform = new RotateTransform(-2.5); + var rect = new Rectangle() + { + RenderTransform = rotateTransform + }; + + var clock = new TestClock(); + var animationRun = animation.RunAsync(rect, clock); + + // position is what you'd expect at end and beginning + clock.Step(TimeSpan.Zero); + Assert.Equal(rotateTransform.Angle, -2.5); + clock.Step(TimeSpan.FromSeconds(5)); + Assert.Equal(rotateTransform.Angle, 2.5); + + // test some points in between end and beginning + var tolerance = 0.01; + clock.Step(TimeSpan.Parse("00:00:10.0153932")); + var expected = -2.4122350198982545; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + + clock.Step(TimeSpan.Parse("00:00:11.2655407")); + expected = -0.37153223002125113; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + + clock.Step(TimeSpan.Parse("00:00:12.6158773")); + expected = 0.3967885416786294; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + + clock.Step(TimeSpan.Parse("00:00:14.6495256")); + expected = 1.8016358493761722; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + } } } From c38c7e2d65ddeb84504819412dbe8671725d09fe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 16:47:15 -0300 Subject: [PATCH 28/70] [OSX] implement FullScreen and fix sync of window states. --- native/Avalonia.Native/inc/avalonia-native.h | 1 + native/Avalonia.Native/src/OSX/window.h | 2 + native/Avalonia.Native/src/OSX/window.mm | 126 ++++++++++++++++--- 3 files changed, 114 insertions(+), 15 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index b10db08adc..b0937d35ba 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -135,6 +135,7 @@ enum AvnWindowState Normal, Minimized, Maximized, + FullScreen, }; enum AvnStandardCursorType diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 505900d584..449751b06e 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -33,6 +33,8 @@ struct INSWindowHolder struct IWindowStateChanged { virtual void WindowStateChanged () = 0; + virtual void StartStateTransition () = 0; + virtual void EndStateTransition () = 0; }; #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 109ed63e6f..d7e4680cce 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -408,6 +408,8 @@ private: SystemDecorations _hasDecorations = SystemDecorationsFull; CGRect _lastUndecoratedFrame; AvnWindowState _lastWindowState; + bool _inSetWindowState; + bool _transitioningWindowState; FORWARD_IUNKNOWN() BEGIN_INTERFACE_MAP() @@ -421,6 +423,8 @@ private: ComPtr WindowEvents; WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { + _transitioningWindowState = false; + _inSetWindowState = false; _lastWindowState = Normal; WindowEvents = events; [Window setCanBecomeKeyAndMain]; @@ -457,11 +461,29 @@ private: } } + void StartStateTransition () override + { + _transitioningWindowState = true; + } + + void EndStateTransition () override + { + _transitioningWindowState = false; + } + void WindowStateChanged () override { - AvnWindowState state; - GetWindowState(&state); - WindowEvents->WindowStateChanged(state); + if(!_inSetWindowState && !_transitioningWindowState) + { + AvnWindowState state; + GetWindowState(&state); + + if(_lastWindowState != state) + { + _lastWindowState = state; + WindowEvents->WindowStateChanged(state); + } + } } bool UndecoratedIsMaximized () @@ -586,6 +608,12 @@ private: return E_POINTER; } + if(([Window styleMask] & NSFullScreenWindowMask) == NSFullScreenWindowMask) + { + *ret = FullScreen; + return S_OK; + } + if([Window isMiniaturized]) { *ret = Minimized; @@ -608,12 +636,25 @@ private: { @autoreleasepool { + if(_lastWindowState == state) + { + return S_OK; + } + + _inSetWindowState = true; + + auto currentState = _lastWindowState; _lastWindowState = state; if(_shown) { switch (state) { case Maximized: + if(currentState == FullScreen) + { + [Window toggleFullScreen:nullptr]; + } + lastPositionSet.X = 0; lastPositionSet.Y = 0; @@ -629,15 +670,29 @@ private: break; case Minimized: + if(currentState == FullScreen) + { + [Window toggleFullScreen:nullptr]; + } + [Window miniaturize:Window]; break; - default: + case FullScreen: + [Window toggleFullScreen:nullptr]; + break; + + case Normal: if([Window isMiniaturized]) { [Window deminiaturize:Window]; } + if(currentState == FullScreen) + { + [Window toggleFullScreen:nullptr]; + } + if(IsZoomed()) { DoZoom(); @@ -646,23 +701,17 @@ private: } } + _inSetWindowState = false; + return S_OK; } } virtual void OnResized () override { - if(_shown) + if(_shown && !_inSetWindowState && !_transitioningWindowState) { - auto windowState = [Window isMiniaturized] ? Minimized - : (IsZoomed() ? Maximized : Normal); - - if (windowState != _lastWindowState) - { - _lastWindowState = windowState; - - WindowEvents->WindowStateChanged(windowState); - } + WindowStateChanged(); } } @@ -1378,7 +1427,54 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)windowDidResize:(NSNotification *)notification { - _parent->OnResized(); + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->WindowStateChanged(); + } +} + +- (void)windowWillExitFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->StartStateTransition(); + } +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->EndStateTransition(); + parent->WindowStateChanged(); + } +} + +- (void)windowWillEnterFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->StartStateTransition(); + } +} + +- (void)windowDidEnterFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->EndStateTransition(); + parent->WindowStateChanged(); + } } - (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame From 00ef2fe59000cb1cd5ce1ec381441da8e708da2c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 16:53:03 -0300 Subject: [PATCH 29/70] deminiturize when entering fullscreen if needed. --- native/Avalonia.Native/src/OSX/window.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index d7e4680cce..349db9f1cd 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -679,6 +679,11 @@ private: break; case FullScreen: + if([Window isMiniaturized]) + { + [Window deminiaturize:Window]; + } + [Window toggleFullScreen:nullptr]; break; From 7ba1c9661bd5d3db6b91b898d7e222e447fde841 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Thu, 30 Apr 2020 15:58:21 -0400 Subject: [PATCH 30/70] Add some notes on how I got the KeySpline test values --- .../KeySplineTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs index b98564a03c..df7c0693e1 100644 --- a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs +++ b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs @@ -46,6 +46,31 @@ namespace Avalonia.Animation.UnitTests Assert.Throws(() => keySpline.ControlPointX2 = input); } + /* + To get the test values for the KeySpline test, you can: + 1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations + 2) Add the following xaml somewhere: +