From 12d8daefaf3e55267ffed30bf4bf606cd940ed45 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Oct 2020 18:29:07 +0200 Subject: [PATCH 01/20] Added nullable annotations. To `ScrollContentPresenter` and `IScrollAnchorProvider`. --- .../IScrollAnchorProvider.cs | 6 ++-- .../Presenters/ScrollContentPresenter.cs | 31 ++++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/IScrollAnchorProvider.cs b/src/Avalonia.Controls/IScrollAnchorProvider.cs index 93f3a0abb8..7ba02e99ea 100644 --- a/src/Avalonia.Controls/IScrollAnchorProvider.cs +++ b/src/Avalonia.Controls/IScrollAnchorProvider.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Controls +#nullable enable + +namespace Avalonia.Controls { /// /// Specifies a contract for a scrolling control that supports scroll anchoring. @@ -8,7 +10,7 @@ /// /// The currently chosen anchor element to use for scroll anchoring. /// - IControl CurrentAnchor { get; } + IControl? CurrentAnchor { get; } /// /// Registers a control as a potential scroll anchor candidate. diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 5fcb14c858..2a04558fc8 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -7,6 +7,8 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls.Presenters { /// @@ -64,11 +66,11 @@ namespace Avalonia.Controls.Presenters private bool _arranging; private Size _extent; private Vector _offset; - private IDisposable _logicalScrollSubscription; + private IDisposable? _logicalScrollSubscription; private Size _viewport; - private Dictionary _activeLogicalGestureScrolls; - private List _anchorCandidates; - private (IControl control, Rect bounds) _anchor; + private Dictionary? _activeLogicalGestureScrolls; + private List? _anchorCandidates; + private (IControl? control, Rect bounds) _anchor; /// /// Initializes static members of the class. @@ -90,8 +92,6 @@ namespace Avalonia.Controls.Presenters this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } - internal event EventHandler PreArrange; - /// /// Gets or sets a value indicating whether the content can be scrolled horizontally. /// @@ -138,7 +138,7 @@ namespace Avalonia.Controls.Presenters } /// - IControl IScrollAnchorProvider.CurrentAnchor => _anchor.control; + IControl? IScrollAnchorProvider.CurrentAnchor => _anchor.control; /// /// Attempts to bring a portion of the target visual into view by scrolling the content. @@ -247,11 +247,6 @@ namespace Avalonia.Controls.Presenters /// protected override Size ArrangeOverride(Size finalSize) { - PreArrange?.Invoke(this, new VectorEventArgs - { - Vector = new Vector(finalSize.Width, finalSize.Height), - }); - if (_logicalScrollSubscription != null || Child == null) { return base.ArrangeOverride(finalSize); @@ -350,7 +345,7 @@ namespace Avalonia.Controls.Presenters { var logicalUnits = delta.Y / LogicalScrollItemSize; delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize); - dy = logicalUnits * scrollable.ScrollSize.Height; + dy = logicalUnits * scrollable!.ScrollSize.Height; } else dy = delta.Y; @@ -368,7 +363,7 @@ namespace Avalonia.Controls.Presenters { var logicalUnits = delta.X / LogicalScrollItemSize; delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize); - dx = logicalUnits * scrollable.ScrollSize.Width; + dx = logicalUnits * scrollable!.ScrollSize.Width; } else dx = delta.X; @@ -405,7 +400,7 @@ namespace Avalonia.Controls.Presenters if (Extent.Height > Viewport.Height) { - double height = isLogical ? scrollable.ScrollSize.Height : 50; + double height = isLogical ? scrollable!.ScrollSize.Height : 50; y += -e.Delta.Y * height; y = Math.Max(y, 0); y = Math.Min(y, Extent.Height - Viewport.Height); @@ -413,7 +408,7 @@ namespace Avalonia.Controls.Presenters if (Extent.Width > Viewport.Width) { - double width = isLogical ? scrollable.ScrollSize.Width : 50; + double width = isLogical ? scrollable!.ScrollSize.Width : 50; x += -e.Delta.X * width; x = Math.Max(x, 0); x = Math.Min(x, Extent.Width - Viewport.Width); @@ -441,7 +436,7 @@ namespace Avalonia.Controls.Presenters private void ChildChanged(AvaloniaPropertyChangedEventArgs e) { - UpdateScrollableSubscription((IControl)e.NewValue); + UpdateScrollableSubscription((IControl?)e.NewValue); if (e.OldValue != null) { @@ -449,7 +444,7 @@ namespace Avalonia.Controls.Presenters } } - private void UpdateScrollableSubscription(IControl child) + private void UpdateScrollableSubscription(IControl? child) { var scrollable = child as ILogicalScrollable; From ce3c8316b5cd90db1e293e7cb4bf4a97cae8c823 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Oct 2020 10:46:15 +0200 Subject: [PATCH 02/20] Don't invalidate ItemsRepeater measure on children change. --- src/Avalonia.Controls/Panel.cs | 5 +++++ src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index ccb92dc497..b7eeb065da 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -137,6 +137,11 @@ namespace Avalonia.Controls throw new NotSupportedException(); } + InvalidateMeasureOnChildrenChanged(); + } + + private protected virtual void InvalidateMeasureOnChildrenChanged() + { InvalidateMeasure(); } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 40f1b8dbb9..9ed09df7db 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -267,6 +267,11 @@ namespace Avalonia.Controls return result; } + private protected override void InvalidateMeasureOnChildrenChanged() + { + // Don't invalidate measure when children change. + } + protected override Size MeasureOverride(Size availableSize) { if (_isLayoutInProgress) From e9c990d9082bd9b43d014b61612ba1baf3ab9b52 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Oct 2020 11:23:21 +0200 Subject: [PATCH 03/20] Tweak ScrollContentPresenter anchoring. Using WinUI's `ScrollPresenter` implementation to guide us. --- .../Presenters/ScrollContentPresenter.cs | 110 +++++++++++------- 1 file changed, 68 insertions(+), 42 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 2a04558fc8..b0b52812b9 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -16,6 +16,8 @@ namespace Avalonia.Controls.Presenters /// public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider { + private const double EdgeDetectionTolerance = 0.1; + /// /// Defines the property. /// @@ -70,7 +72,9 @@ namespace Avalonia.Controls.Presenters private Size _viewport; private Dictionary? _activeLogicalGestureScrolls; private List? _anchorCandidates; - private (IControl? control, Rect bounds) _anchor; + private IControl? _anchorElement; + private Rect _anchorElementBounds; + private bool _isAnchorElementDirty; /// /// Initializes static members of the class. @@ -138,7 +142,14 @@ namespace Avalonia.Controls.Presenters } /// - IControl? IScrollAnchorProvider.CurrentAnchor => _anchor.control; + IControl? IScrollAnchorProvider.CurrentAnchor + { + get + { + EnsureAnchorElementSelection(); + return _anchorElement; + } + } /// /// Attempts to bring a portion of the target visual into view by scrolling the content. @@ -215,16 +226,18 @@ namespace Avalonia.Controls.Presenters _anchorCandidates ??= new List(); _anchorCandidates.Add(element); + _isAnchorElementDirty = true; } /// void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element) { _anchorCandidates?.Remove(element); + _isAnchorElementDirty = true; - if (_anchor.control == element) + if (_anchorElement == element) { - _anchor = default; + _anchorElement = null; } } @@ -266,59 +279,69 @@ namespace Avalonia.Controls.Presenters // If we have an anchor and its position relative to Child has changed during the // arrange then that change wasn't just due to scrolling (as scrolling doesn't adjust // relative positions within Child). - if (_anchor.control != null && - TranslateBounds(_anchor.control, Child, out var updatedBounds) && - updatedBounds.Position != _anchor.bounds.Position) + if (_anchorElement != null && + TranslateBounds(_anchorElement, Child, out var updatedBounds) && + updatedBounds.Position != _anchorElementBounds.Position) { - var offset = updatedBounds.Position - _anchor.bounds.Position; + var offset = updatedBounds.Position - _anchorElementBounds.Position; return offset; } return default; } - // Calculate the new anchor element. - _anchor = CalculateCurrentAnchor(); + var isAnchoring = Offset.X >= EdgeDetectionTolerance || Offset.Y >= EdgeDetectionTolerance; - // Do the arrange. - ArrangeOverrideImpl(size, -Offset); + if (isAnchoring) + { + // Calculate the new anchor element if necessary. + EnsureAnchorElementSelection(); - // If the anchor moved during the arrange, we need to adjust the offset and do another arrange. - var anchorShift = TrackAnchor(); + // Do the arrange. + ArrangeOverrideImpl(size, -Offset); - if (anchorShift != default) - { - var newOffset = Offset + anchorShift; - var newExtent = Extent; - var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height); + // If the anchor moved during the arrange, we need to adjust the offset and do another arrange. + var anchorShift = TrackAnchor(); - if (newOffset.X > maxOffset.X) + if (anchorShift != default) { - newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width); - } + var newOffset = Offset + anchorShift; + var newExtent = Extent; + var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height); - if (newOffset.Y > maxOffset.Y) - { - newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height); - } + if (newOffset.X > maxOffset.X) + { + newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width); + } - Extent = newExtent; + if (newOffset.Y > maxOffset.Y) + { + newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height); + } - try - { - _arranging = true; - Offset = newOffset; - } - finally - { - _arranging = false; + Extent = newExtent; + + try + { + _arranging = true; + Offset = newOffset; + } + finally + { + _arranging = false; + } + + ArrangeOverrideImpl(size, -Offset); } - + } + else + { ArrangeOverrideImpl(size, -Offset); } Viewport = finalSize; Extent = Child.Bounds.Size.Inflate(Child.Margin); + _isAnchorElementDirty = true; return finalSize; } @@ -493,13 +516,17 @@ namespace Avalonia.Controls.Presenters } } - private (IControl, Rect) CalculateCurrentAnchor() + private void EnsureAnchorElementSelection() { - if (_anchorCandidates == null) + if (!_isAnchorElementDirty || _anchorCandidates is null) { - return default; + return; } + _anchorElement = null; + _anchorElementBounds = default; + _isAnchorElementDirty = false; + var bestCandidate = default(IControl); var bestCandidateDistance = double.MaxValue; @@ -526,10 +553,9 @@ namespace Avalonia.Controls.Presenters // bounds aren't relative to the ScrollContentPresenter itself, if they change // then we know it wasn't just due to scrolling. var unscrolledBounds = TranslateBounds(bestCandidate, Child); - return (bestCandidate, unscrolledBounds); + _anchorElement = bestCandidate; + _anchorElementBounds = unscrolledBounds; } - - return default; } private bool GetViewportBounds(IControl element, out Rect bounds) From d5224d4bb0a98f55c1da1cae9eaab6a57d4b3697 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Oct 2020 11:43:56 +0200 Subject: [PATCH 04/20] Ensure valid index before calculating offset. --- src/Avalonia.Layout/StackLayout.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index 909c7bc7eb..4a93c8344f 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -249,8 +249,8 @@ namespace Avalonia.Layout realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize) { anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize); - offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent); anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex)); + offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent); } } From bba2cafcc1efe44af5e7cf286a2bedaa389cdc20 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Oct 2020 16:13:21 +0200 Subject: [PATCH 05/20] Don't register anchor candidates until they're laid out. --- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 13 +++++++++++-- src/Avalonia.Controls/Repeater/ViewManager.cs | 2 +- src/Avalonia.Controls/Repeater/ViewportManager.cs | 14 ++++++++++++-- .../Repeater/VirtualizationInfo.cs | 1 + 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 9ed09df7db..fb2da09e73 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -369,6 +369,12 @@ namespace Avalonia.Controls { var newBounds = element.Bounds; virtInfo.ArrangeBounds = newBounds; + + if (!virtInfo.IsRegisteredAsAnchorCandidate) + { + _viewportManager.RegisterScrollAnchorCandidate(element); + virtInfo.IsRegisteredAsAnchorCandidate = true; + } } } @@ -520,11 +526,14 @@ namespace Avalonia.Controls return element; } - internal void OnElementPrepared(IControl element, int index) + internal void OnElementPrepared(IControl element, VirtualizationInfo virtInfo) { - _viewportManager.OnElementPrepared(element); + _viewportManager.OnElementPrepared(element, virtInfo); + if (ElementPrepared != null) { + var index = virtInfo.Index; + if (_elementPreparedArgs == null) { _elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index); diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index 416b1e2824..cf2066b373 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -661,7 +661,7 @@ namespace Avalonia.Controls children.Add(element); } - repeater.OnElementPrepared(element, index); + repeater.OnElementPrepared(element, virtInfo); // Update realized indices _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index bdb0fa3270..6e24408aa9 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -240,9 +240,14 @@ namespace Avalonia.Controls } } - public void OnElementPrepared(IControl element) + public void OnElementPrepared(IControl element, VirtualizationInfo virtInfo) { - _scroller?.RegisterAnchorCandidate(element); + // WinUI registers the element as an anchor candidate here, but I feel that's in error: + // at this point the element has not yet been positioned by the arrange pass so it will + // have its previous position, meaning that when the arrange pass moves it into its new + // position, an incorrect scroll anchoring will occur. Instead signal that it's not yet + // registered as a scroll anchor candidate. + virtInfo.IsRegisteredAsAnchorCandidate = false; } public void OnElementCleared(IControl element) @@ -373,6 +378,11 @@ namespace Avalonia.Controls } } + public void RegisterScrollAnchorCandidate(IControl element) + { + _scroller?.RegisterAnchorCandidate(element); + } + private IControl GetImmediateChildOfRepeater(IControl descendant) { var targetChild = descendant; diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs index 7a639419c1..f8cfde609e 100644 --- a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs @@ -38,6 +38,7 @@ namespace Avalonia.Controls public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool; public bool MustClearDataContext { get; set; } public bool KeepAlive { get; set; } + public bool IsRegisteredAsAnchorCandidate { get; set; } public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory; public string UniqueId { get; private set; } From 73ab16e68c5ae1a55738d628ac9d43d292dec3b4 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 8 Oct 2020 12:00:26 +0200 Subject: [PATCH 06/20] Fix Direct2D1 text rendering --- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 34 +++- .../Media/TextShaperImpl.cs | 160 ++++++++++-------- 2 files changed, 121 insertions(+), 73 deletions(-) diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index ae927d44a5..9c2ca053e6 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -238,22 +238,44 @@ namespace Avalonia.Direct2D1 width = 0; - for (var i = 0; i < glyphCount; i++) + if (glyphRun.GlyphAdvances.IsEmpty) + { + for (var i = 0; i < glyphCount; i++) + { + var advance = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]); + + run.Advances[i] = advance; + + width += advance; + } + } + else + { + for (var i = 0; i < glyphCount; i++) + { + var advance = (float)glyphRun.GlyphAdvances[i]; + + run.Advances[i] = advance; + + width += advance; + } + } + + if (glyphRun.GlyphOffsets.IsEmpty) { - run.Advances[i] = (float)glyphRun.GlyphAdvances[i]; - width += run.Advances[i]; + return new GlyphRunImpl(run); } run.Offsets = new GlyphOffset[glyphCount]; for (var i = 0; i < glyphCount; i++) { - var offset = glyphRun.GlyphOffsets[i]; + var (x, y) = glyphRun.GlyphOffsets[i]; run.Offsets[i] = new GlyphOffset { - AdvanceOffset = (float)offset.X, - AscenderOffset = (float)offset.Y + AdvanceOffset = (float)x, + AscenderOffset = (float)y }; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 254b5684a4..20b09a9aac 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -1,6 +1,6 @@ -using System.Globalization; +using System; +using System.Globalization; using Avalonia.Media; -using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; using Avalonia.Utilities; @@ -15,51 +15,9 @@ namespace Avalonia.Direct2D1.Media { using (var buffer = new Buffer()) { - buffer.ContentType = ContentType.Unicode; + FillBuffer(buffer, text); - var breakCharPosition = text.Length - 1; - - var codepoint = Codepoint.ReadAt(text, breakCharPosition, out var count); - - if (codepoint.IsBreakChar) - { - var breakCharCount = 1; - - if (text.Length > 1) - { - var previousCodepoint = Codepoint.ReadAt(text, breakCharPosition - count, out _); - - if (codepoint == '\r' && previousCodepoint == '\n' - || codepoint == '\n' && previousCodepoint == '\r') - { - breakCharCount = 2; - } - } - - if (breakCharPosition != text.Start) - { - buffer.AddUtf16(text.Buffer.Span.Slice(0, text.Length - breakCharCount)); - } - - var cluster = buffer.GlyphInfos.Length > 0 ? - buffer.GlyphInfos[buffer.Length - 1].Cluster + 1 : - (uint)text.Start; - - switch (breakCharCount) - { - case 1: - buffer.Add('\u200C', cluster); - break; - case 2: - buffer.Add('\u200C', cluster); - buffer.Add('\u200D', cluster); - break; - } - } - else - { - buffer.AddUtf16(text.Buffer.Span); - } + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); buffer.GuessSegmentProperties(); @@ -67,44 +25,38 @@ namespace Avalonia.Direct2D1.Media var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); - font.Shape(buffer); font.GetScale(out var scaleX, out _); var textScale = fontRenderingEmSize / scaleX; - var len = buffer.Length; + var bufferLength = buffer.Length; - var info = buffer.GetGlyphInfoSpan(); + var glyphInfos = buffer.GetGlyphInfoSpan(); - var pos = buffer.GetGlyphPositionSpan(); + var glyphPositions = buffer.GetGlyphPositionSpan(); - var glyphIndices = new ushort[len]; + var glyphIndices = new ushort[bufferLength]; - var clusters = new ushort[len]; + var clusters = new ushort[bufferLength]; - var glyphAdvances = new double[len]; + double[] glyphAdvances = null; - var glyphOffsets = new Vector[len]; + Vector[] glyphOffsets = null; - for (var i = 0; i < len; i++) + for (var i = 0; i < bufferLength; i++) { - glyphIndices[i] = (ushort)info[i].Codepoint; - - clusters[i] = (ushort)(text.Start + info[i].Cluster); - - var advanceX = pos[i].XAdvance * textScale; - // Depends on direction of layout - //var advanceY = pos[i].YAdvance * textScale; + glyphIndices[i] = (ushort)glyphInfos[i].Codepoint; - glyphAdvances[i] = advanceX; + clusters[i] = (ushort)glyphInfos[i].Cluster; - var offsetX = pos[i].XOffset * textScale; - var offsetY = pos[i].YOffset * textScale; + if (!glyphTypeface.IsFixedPitch) + { + SetAdvance(glyphPositions, i, textScale, ref glyphAdvances); + } - glyphOffsets[i] = new Vector(offsetX, offsetY); + SetOffset(glyphPositions, i, textScale, ref glyphOffsets); } return new GlyphRun(glyphTypeface, fontRenderingEmSize, @@ -115,5 +67,79 @@ namespace Avalonia.Direct2D1.Media new ReadOnlySlice(clusters)); } } + + private static void FillBuffer(Buffer buffer, ReadOnlySlice text) + { + buffer.ContentType = ContentType.Unicode; + + var i = 0; + + while (i < text.Length) + { + var codepoint = Codepoint.ReadAt(text, i, out var count); + + var cluster = (uint)(text.Start + i); + + if (codepoint.IsBreakChar) + { + if (i + 1 < text.Length) + { + var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _); + + if (nextCodepoint == '\r' && codepoint == '\n' || nextCodepoint == '\n' && codepoint == '\r') + { + count++; + + buffer.Add('\u200C', cluster); + + buffer.Add('\u200D', cluster); + } + else + { + buffer.Add('\u200C', cluster); + } + } + else + { + buffer.Add('\u200C', cluster); + } + } + else + { + buffer.Add(codepoint, cluster); + } + + i += count; + } + } + + private static void SetOffset(ReadOnlySpan glyphPositions, int index, double textScale, + ref Vector[] offsetBuffer) + { + var position = glyphPositions[index]; + + if (position.XOffset == 0 && position.YOffset == 0) + { + return; + } + + offsetBuffer ??= new Vector[glyphPositions.Length]; + + var offsetX = position.XOffset * textScale; + + var offsetY = position.YOffset * textScale; + + offsetBuffer[index] = new Vector(offsetX, offsetY); + } + + private static void SetAdvance(ReadOnlySpan glyphPositions, int index, double textScale, + ref double[] advanceBuffer) + { + advanceBuffer ??= new double[glyphPositions.Length]; + + // Depends on direction of layout + // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale; + advanceBuffer[index] = glyphPositions[index].XAdvance * textScale; + } } } From d57ad558d2dd2bc86fa2271c8ec02881e3dc40ac Mon Sep 17 00:00:00 2001 From: Will Kennedy Date: Sun, 11 Oct 2020 00:27:07 -0400 Subject: [PATCH 07/20] Fix IsChecked property differing from :checked pseudoclass --- src/Avalonia.Controls/Primitives/ToggleButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index f96ca9310d..6b2c566422 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -94,7 +94,7 @@ namespace Avalonia.Controls.Primitives set { SetAndRaise(IsCheckedProperty, ref _isChecked, value); - UpdatePseudoClasses(value); + UpdatePseudoClasses(IsChecked); } } From cb6785e7007e35d1df99775fec86b9d916a545b1 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 12 Oct 2020 14:51:10 +0200 Subject: [PATCH 08/20] Properly handle multiple line breaks --- src/Avalonia.Visuals/Media/GlyphRun.cs | 4 ++-- .../Media/TextFormatting/TextFormatterImpl.cs | 8 ------- .../Media/TextFormatting/TextLayoutTests.cs | 22 ++++++++++++++++++- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 66a8c1dd0c..af228ec57b 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -399,14 +399,14 @@ namespace Avalonia.Media if (characterIndex > GlyphClusters[GlyphClusters.Length - 1]) { - return _glyphClusters.End; + return _glyphClusters.Length - 1; } } else { if (characterIndex < GlyphClusters[GlyphClusters.Length - 1]) { - return _glyphClusters.End; + return _glyphClusters.Length - 1; } if (characterIndex > GlyphClusters[0]) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 394dae8253..6ae5258323 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -339,14 +339,6 @@ namespace Avalonia.Media.TextFormatting return true; } - //The line breaker isn't treating \n\r as a pair so we have to fix that here. - if (textRun.Text[lineBreak.PositionMeasure] == '\n' - && textRun.Text[lineBreak.PositionWrap] == '\r') - { - lineBreak = new LineBreak(lineBreak.PositionMeasure, lineBreak.PositionWrap + 1, - lineBreak.Required); - } - return true; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 26e8ce4797..f7bc75c05d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -417,7 +417,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting [Theory] [InlineData("abcde\r\n", 7)] // Carriage Return + Line Feed - [InlineData("abcde\n\r", 7)] // This isn't valid but we somehow have to support it. [InlineData("abcde\u000A", 6)] // Line Feed [InlineData("abcde\u000B", 6)] // Vertical Tab [InlineData("abcde\u000C", 6)] // Form Feed @@ -575,6 +574,27 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Process_Multiple_NewLines_Properly() + { + using (Start()) + { + var text = "123\r\n\r\n456\r\n\r\n"; + var layout = new TextLayout( + text, + Typeface.Default, + 12.0f, + Brushes.Black); + + Assert.Equal(5, layout.TextLines.Count); + + Assert.Equal("123\r\n", layout.TextLines[0].TextRuns[0].Text); + Assert.Equal("\r\n", layout.TextLines[1].TextRuns[0].Text); + Assert.Equal("456\r\n", layout.TextLines[2].TextRuns[0].Text); + Assert.Equal("\r\n", layout.TextLines[3].TextRuns[0].Text); + } + } + [Fact] public void Should_Wrap_Min_OneCharacter_EveryLine() { From 94b6f133ea30f1fb6661403d5f46daa4ea45265d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 12 Oct 2020 17:30:36 +0200 Subject: [PATCH 09/20] Added failing test for #4822. --- .../Shapes/PathTests.cs | 22 ++++++++++++++++++- .../MockPlatformRenderInterface.cs | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs index 88c64e76cc..2a0bcc8d39 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls.Shapes; +using System.Runtime.InteropServices; +using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -34,5 +35,24 @@ namespace Avalonia.Controls.UnitTests.Shapes root.Child = null; } + + [Fact] + public void Arrange_Without_Measure_Updates_RenderedGeometry_Transform() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new Path + { + Data = new RectangleGeometry { Rect = new Rect(0, 0, 100, 100) }, + Stretch = Stretch.Fill, + }; + + target.Measure(new Size(200, 200)); + target.Arrange(new Rect(0, 0, 200, 200)); + Assert.Equal(Matrix.CreateScale(2, 2), target.RenderedGeometry.Transform.Value); + + target.Arrange(new Rect(0, 0, 300, 300)); + Assert.Equal(Matrix.CreateScale(3, 3), target.RenderedGeometry.Transform.Value); + } } } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 08df23cbe1..e73a76357a 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -34,7 +34,7 @@ namespace Avalonia.UnitTests public IGeometryImpl CreateRectangleGeometry(Rect rect) { - return Mock.Of(); + return Mock.Of(x => x.Bounds == rect); } public IRenderTarget CreateRenderTarget(IEnumerable surfaces) From d631474ddf75f6ec5a41488aac4339fe4eb34a10 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 12 Oct 2020 15:10:06 +0300 Subject: [PATCH 10/20] add failing test for autoscroll to selected item in listbox #4855 --- .../ListBoxTests.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 2e2ccf7326..145fce4fed 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -407,6 +407,53 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, raised); } + [Fact] + public void Adding_And_Selecting_Item_With_AutoScrollToSelectedItem_Should_NotHide_FirstItem() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new AvaloniaList(); + + var wnd = new Window() { Width = 100, Height = 100, IsVisible = true }; + + var target = new ListBox() + { + VerticalAlignment = Layout.VerticalAlignment.Top, + AutoScrollToSelectedItem = true, + Width = 50, + VirtualizationMode = ItemVirtualizationMode.Simple, + ItemTemplate = new FuncDataTemplate((c, _) => new Border() { Height = 10 }), + Items = items, + }; + wnd.Content = target; + + var lm = wnd.LayoutManager; + + lm.ExecuteInitialLayoutPass(); + + var panel = target.Presenter.Panel; + + items.Add("Item 1"); + target.Selection.Select(0); + lm.ExecuteLayoutPass(); + + Assert.Equal(1, panel.Children.Count); + + items.Add("Item 2"); + target.Selection.Select(1); + lm.ExecuteLayoutPass(); + + Assert.Equal(2, panel.Children.Count); + + //make sure we have enough space to show all items + Assert.True(panel.Bounds.Height >= panel.Children.Sum(c => c.Bounds.Height)); + + //make sure we show items and they completelly visible, not only partially + Assert.True(panel.Children[0].Bounds.Top >= 0 && panel.Children[0].Bounds.Bottom <= panel.Bounds.Height, "first item is not completelly visible!"); + Assert.True(panel.Children[1].Bounds.Top >= 0 && panel.Children[1].Bounds.Bottom <= panel.Bounds.Height, "second item is not completelly visible!"); + } + } + private FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate((parent, scope) => From b894a812d356d13da07c57ec597fd0b58ae8812c Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 12 Oct 2020 23:59:07 +0300 Subject: [PATCH 11/20] fix #4855 before try to scroll invalidate panel --- src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 3fac440c40..bdc68bee7e 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -512,6 +512,14 @@ namespace Avalonia.Controls.Presenters var generator = Owner.ItemContainerGenerator; var newOffset = -1.0; + if (!panel.IsMeasureValid && panel.PreviousMeasure.HasValue) + { + //before any kind of scrolling we need to make sure panel measure is valid + //or we risk get panel into not valid state + //we make a preemptive quick measure so scrolling is valid + panel.Measure(panel.PreviousMeasure.Value); + } + if (index >= 0 && index < ItemCount) { if (index <= FirstIndex) From 93f215d8eaca8fa1dd9f04fb562055f338afae02 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 13 Oct 2020 16:47:50 +0200 Subject: [PATCH 12/20] More tests for #4822. Tested WPF's `Path` to confirm expected results: https://github.com/wieslawsoltes/WpfUnitTests --- .../Shapes/PathTests.cs | 108 +++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs index 2a0bcc8d39..a10a03a5ae 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs @@ -1,5 +1,4 @@ -using System.Runtime.InteropServices; -using Avalonia.Controls.Shapes; +using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -36,6 +35,111 @@ namespace Avalonia.Controls.UnitTests.Shapes root.Child = null; } + [Theory] + [InlineData(Stretch.None, 100, 200)] + [InlineData(Stretch.Fill, 500, 500)] + [InlineData(Stretch.Uniform, 250, 500)] + [InlineData(Stretch.UniformToFill, 500, 500)] + public void Calculates_Correct_DesiredSize_For_Finite_Bounds(Stretch stretch, double expectedWidth, double expectedHeight) + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new Path() + { + Data = new RectangleGeometry { Rect = new Rect(0, 0, 100, 200) }, + Stretch = stretch, + }; + + target.Measure(new Size(500, 500)); + + Assert.Equal(new Size(expectedWidth, expectedHeight), target.DesiredSize); + } + + [Theory] + [InlineData(Stretch.None)] + [InlineData(Stretch.Fill)] + [InlineData(Stretch.Uniform)] + [InlineData(Stretch.UniformToFill)] + public void Calculates_Correct_DesiredSize_For_Infinite_Bounds(Stretch stretch) + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new Path() + { + Data = new RectangleGeometry { Rect = new Rect(0, 0, 100, 200) }, + Stretch = stretch, + }; + + target.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + + Assert.Equal(new Size(100, 200), target.DesiredSize); + } + + [Fact] + public void Measure_Does_Not_Update_RenderedGeometry_Transform() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new Path + { + Data = new RectangleGeometry { Rect = new Rect(0, 0, 100, 200) }, + Stretch = Stretch.Fill, + }; + + target.Measure(new Size(500, 500)); + + Assert.Null(target.RenderedGeometry.Transform); + } + + [Theory] + [InlineData(Stretch.None, 1, 1)] + [InlineData(Stretch.Fill, 5, 2.5)] + [InlineData(Stretch.Uniform, 2.5, 2.5)] + [InlineData(Stretch.UniformToFill, 5, 5)] + public void Arrange_Updates_RenderedGeometry_Transform(Stretch stretch, double expectedScaleX, double expectedScaleY) + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new Path + { + Data = new RectangleGeometry { Rect = new Rect(0, 0, 100, 200) }, + Stretch = stretch, + }; + + target.Measure(new Size(500, 500)); + target.Arrange(new Rect(0, 0, 500, 500)); + + if (expectedScaleX == 1 && expectedScaleY == 1) + { + Assert.Null(target.RenderedGeometry.Transform); + } + else + { + Assert.Equal(Matrix.CreateScale(expectedScaleX, expectedScaleY), target.RenderedGeometry.Transform.Value); + } + } + + [Fact] + public void Measure_Without_Arrange_Does_Not_Clear_RenderedGeometry_Transform() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new Path + { + Data = new RectangleGeometry { Rect = new Rect(0, 0, 100, 100) }, + Stretch = Stretch.Fill, + }; + + target.Measure(new Size(200, 200)); + target.Arrange(new Rect(0, 0, 200, 200)); + + Assert.Equal(Matrix.CreateScale(2, 2), target.RenderedGeometry.Transform.Value); + + target.Measure(new Size(300, 300)); + + Assert.Equal(Matrix.CreateScale(2, 2), target.RenderedGeometry.Transform.Value); + } + [Fact] public void Arrange_Without_Measure_Updates_RenderedGeometry_Transform() { From beada97aabaf46f44564f4bcdcfe2100c1421ddc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 13 Oct 2020 16:48:06 +0200 Subject: [PATCH 13/20] Set RenderedGeometry.Transform on arrange. Fixes #4822 but breaks a bunch of other tests. --- src/Avalonia.Controls/Shapes/Shape.cs | 39 +++------------------------ 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 7d1525afc4..67a7d48c61 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -248,52 +248,21 @@ namespace Avalonia.Controls.Shapes protected override Size MeasureOverride(Size availableSize) { - bool deferCalculateTransform; - switch (Stretch) + if (DefiningGeometry is null) { - case Stretch.Fill: - case Stretch.UniformToFill: - deferCalculateTransform = double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height); - break; - case Stretch.Uniform: - deferCalculateTransform = double.IsInfinity(availableSize.Width) && double.IsInfinity(availableSize.Height); - break; - case Stretch.None: - default: - deferCalculateTransform = false; - break; + return default; } - if (deferCalculateTransform) - { - _calculateTransformOnArrange = true; - return DefiningGeometry?.Bounds.Size ?? Size.Empty; - } - else - { - _calculateTransformOnArrange = false; - return CalculateShapeSizeAndSetTransform(availableSize); - } + return CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch).Item1; } protected override Size ArrangeOverride(Size finalSize) - { - if (_calculateTransformOnArrange) - { - _calculateTransformOnArrange = false; - CalculateShapeSizeAndSetTransform(finalSize); - } - - return finalSize; - } - - private Size CalculateShapeSizeAndSetTransform(Size availableSize) { if (DefiningGeometry != null) { // This should probably use GetRenderBounds(strokeThickness) but then the calculations // will multiply the stroke thickness as well, which isn't correct. - var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch); + var (size, transform) = CalculateSizeAndTransform(finalSize, DefiningGeometry.Bounds, Stretch); if (_transform != transform) { From 51437baf7301e9313f6727c4075cd0014b21bd10 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 13 Oct 2020 17:31:35 +0200 Subject: [PATCH 14/20] Properly scale fixed pitch fonts --- src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 9c2ca053e6..6ae27870e8 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -238,11 +238,13 @@ namespace Avalonia.Direct2D1 width = 0; + var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); + if (glyphRun.GlyphAdvances.IsEmpty) { for (var i = 0; i < glyphCount; i++) { - var advance = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]); + var advance = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; run.Advances[i] = advance; From 9f8f4b224f188d2cf48d9a613291b3dbb51dcc04 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 13 Oct 2020 17:42:42 +0200 Subject: [PATCH 15/20] Provide a RenderInterface for controls which use shapes. --- tests/Avalonia.Controls.UnitTests/CanvasTests.cs | 9 +++++++++ .../LayoutTransformControlTests.cs | 15 +++++++++++++++ .../RelativePanelTests.cs | 6 ++++++ tests/Avalonia.Controls.UnitTests/ViewboxTests.cs | 13 +++++++++++++ 4 files changed, 43 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/CanvasTests.cs b/tests/Avalonia.Controls.UnitTests/CanvasTests.cs index da1698330f..11a349f53e 100644 --- a/tests/Avalonia.Controls.UnitTests/CanvasTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CanvasTests.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls.Shapes; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests @@ -9,6 +10,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Left_Property_Should_Work() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + Rectangle rect; var target = new Canvas { @@ -34,6 +37,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Top_Property_Should_Work() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + Rectangle rect; var target = new Canvas { @@ -59,6 +64,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Right_Property_Should_Work() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + Rectangle rect; var target = new Canvas { @@ -84,6 +91,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Bottom_Property_Should_Work() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + Rectangle rect; var target = new Canvas { diff --git a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs index 13c946b549..60139c2881 100644 --- a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs @@ -1,5 +1,6 @@ using Avalonia.Controls.Shapes; using Avalonia.Media; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests @@ -171,6 +172,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Should_Generate_RotateTransform_90_degrees() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform( 100, 25, @@ -193,6 +196,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Should_Generate_RotateTransform_minus_90_degrees() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform( 100, 25, @@ -215,6 +220,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Should_Generate_ScaleTransform_x2() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform( 100, 50, @@ -236,6 +243,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Should_Generate_SkewTransform_45_degrees() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform( 100, 100, @@ -258,6 +267,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Should_Generate_SkewTransform_minus_45_degrees() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform( 100, 100, @@ -279,6 +290,8 @@ namespace Avalonia.Controls.UnitTests private static void TransformMeasureSizeTest(Size size, Transform transform, Size expectedSize) { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform( size.Width, size.Height, @@ -292,6 +305,8 @@ namespace Avalonia.Controls.UnitTests private static void TransformRootBoundsTest(Size size, Transform transform, Rect expectedBounds) { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(size.Width, size.Height, transform); Rect outBounds = lt.TransformRoot.Bounds; diff --git a/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs b/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs index 6e171a58e7..6b2f05c923 100644 --- a/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs @@ -1,4 +1,5 @@ using Avalonia.Controls.Shapes; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests @@ -8,6 +9,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Lays_Out_1_Child_Next_the_other() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); var rect1 = new Rectangle { Height = 20, Width = 20 }; var rect2 = new Rectangle { Height = 20, Width = 20 }; @@ -34,6 +36,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Lays_Out_1_Child_Below_the_other() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); var rect1 = new Rectangle { Height = 20, Width = 20 }; var rect2 = new Rectangle { Height = 20, Width = 20 }; @@ -60,6 +63,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void RelativePanel_Can_Center() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); var rect1 = new Rectangle { Height = 20, Width = 20 }; var rect2 = new Rectangle { Height = 20, Width = 20 }; @@ -86,6 +90,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void LeftOf_Measures_Correctly() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); var rect1 = new Rectangle { Height = 20, Width = 20 }; var rect2 = new Rectangle { Height = 20, Width = 20 }; @@ -111,6 +116,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Above_Measures_Correctly() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); var rect1 = new Rectangle { Height = 20, Width = 20 }; var rect2 = new Rectangle { Height = 20, Width = 20 }; diff --git a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs index ad0f318d2f..e005bafbf9 100644 --- a/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ViewboxTests.cs @@ -1,5 +1,6 @@ using Avalonia.Controls.Shapes; using Avalonia.Media; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests @@ -9,6 +10,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Viewbox_Stretch_Uniform_Child() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } }; target.Measure(new Size(200, 200)); @@ -25,6 +28,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Viewbox_Stretch_None_Child() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + var target = new Viewbox() { Stretch = Stretch.None, Child = new Rectangle() { Width = 100, Height = 50 } }; target.Measure(new Size(200, 200)); @@ -41,6 +46,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Viewbox_Stretch_Fill_Child() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + var target = new Viewbox() { Stretch = Stretch.Fill, Child = new Rectangle() { Width = 100, Height = 50 } }; target.Measure(new Size(200, 200)); @@ -57,6 +64,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Viewbox_Stretch_UniformToFill_Child() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + var target = new Viewbox() { Stretch = Stretch.UniformToFill, Child = new Rectangle() { Width = 100, Height = 50 } }; target.Measure(new Size(200, 200)); @@ -73,6 +82,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Viewbox_Stretch_Uniform_Child_With_Unrestricted_Width() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } }; target.Measure(new Size(double.PositiveInfinity, 200)); @@ -89,6 +100,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Viewbox_Stretch_Uniform_Child_With_Unrestricted_Height() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + var target = new Viewbox() { Child = new Rectangle() { Width = 100, Height = 50 } }; target.Measure(new Size(200, double.PositiveInfinity)); From 10b6cfc0117427bda643b4ba9716df7dfa52e495 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 13 Oct 2020 17:43:24 +0200 Subject: [PATCH 16/20] Reserve all of arrange rect. --- src/Avalonia.Controls/Shapes/Shape.cs | 3 +-- .../Shapes/PathTests.cs | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 67a7d48c61..28206a398a 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -62,7 +62,6 @@ namespace Avalonia.Controls.Shapes private Matrix _transform = Matrix.Identity; private Geometry? _definingGeometry; private Geometry? _renderedGeometry; - private bool _calculateTransformOnArrange; static Shape() { @@ -270,7 +269,7 @@ namespace Avalonia.Controls.Shapes _renderedGeometry = null; } - return size; + return finalSize; } return Size.Empty; diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs index a10a03a5ae..8b8656a76b 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs @@ -119,6 +119,26 @@ namespace Avalonia.Controls.UnitTests.Shapes } } + [Fact] + public void Arrange_Reserves_All_Of_Arrange_Rect() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + RectangleGeometry geometry; + var target = new Path + { + Data = geometry = new RectangleGeometry { Rect = new Rect(0, 0, 100, 200) }, + Stretch = Stretch.Uniform, + }; + + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(0, 0, 400, 400)); + + Assert.Equal(new Rect(0, 0, 100, 200), geometry.Rect); + Assert.Equal(Matrix.CreateScale(2, 2), target.RenderedGeometry.Transform.Value); + Assert.Equal(new Rect(0, 0, 400, 400), target.Bounds); + } + [Fact] public void Measure_Without_Arrange_Does_Not_Clear_RenderedGeometry_Transform() { From 67215a85904eb250e979a905a3728c0fffff2a8c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 13 Oct 2020 18:07:14 +0200 Subject: [PATCH 17/20] Removed outdated tests. --- .../ShapeLayoutTests.cs | 113 ------------------ 1 file changed, 113 deletions(-) delete mode 100644 tests/Avalonia.Layout.UnitTests/ShapeLayoutTests.cs diff --git a/tests/Avalonia.Layout.UnitTests/ShapeLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/ShapeLayoutTests.cs deleted file mode 100644 index 2ccd6fb04c..0000000000 --- a/tests/Avalonia.Layout.UnitTests/ShapeLayoutTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Controls; -using Avalonia.Controls.Shapes; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.UnitTests; -using Xunit; - -namespace Avalonia.Layout.UnitTests - -{ - public class ShapeLayoutTests : TestWithServicesBase - { - - public ShapeLayoutTests() - { - AvaloniaLocator.CurrentMutable - .Bind().ToSingleton(); - } - - [Fact] - public void Shape_Transformation_Calculation_Should_Be_Deferred_To_Arrange_When_Strech_Is_Fill_And_Aviable_Size_Is_Infinite() - { - var shape = new Polygon() - { - Points = new List - { - new Point(0, 0), - new Point(10, 5), - new Point(0, 10) - }, - Stretch = Stretch.Fill - }; - - var availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity); - shape.Measure(availableSize); - Geometry postMeasureGeometry = shape.RenderedGeometry; - Transform postMeasureTransform = postMeasureGeometry.Transform; - - var finalSize = new Size(100, 50); - var finalRect = new Rect(finalSize); - shape.Arrange(finalRect); - - Geometry postArrangeGeometry = shape.RenderedGeometry; - Transform postArrangeTransform = postArrangeGeometry.Transform; - - Assert.NotEqual(postMeasureGeometry, postArrangeGeometry); - Assert.NotEqual(postMeasureTransform, postArrangeTransform); - Assert.Equal(finalSize, shape.Bounds.Size); - } - - [Fact] - public void Shape_Transformation_Calculation_Should_Not_Be_Deferred_To_Arrange_When_Strech_Is_Fill_And_Aviable_Size_Is_Finite() - { - var shape = new Polygon() - { - Points = new List - { - new Point(0, 0), - new Point(10, 5), - new Point(0, 10) - }, - Stretch = Stretch.Fill - }; - - var availableSize = new Size(100, 50); - shape.Measure(availableSize); - Geometry postMeasureGeometry = shape.RenderedGeometry; - Transform postMeasureTransform = postMeasureGeometry.Transform; - - var finalRect = new Rect(availableSize); - shape.Arrange(finalRect); - - Geometry postArrangeGeometry = shape.RenderedGeometry; - Transform postArrangeTransform = postArrangeGeometry.Transform; - - Assert.Equal(postMeasureGeometry, postArrangeGeometry); - Assert.Equal(postMeasureTransform, postArrangeTransform); - Assert.Equal(availableSize, shape.Bounds.Size); - } - - [Fact] - public void Shape_Transformation_Calculation_Should_Not_Be_Deferred_To_Arrange_When_Strech_Is_None() - { - var shape = new Polygon() - { - Points = new List - { - new Point(0, 0), - new Point(10, 5), - new Point(0, 10) - }, - Stretch = Stretch.None - }; - - var availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity); - shape.Measure(availableSize); - Geometry postMeasureGeometry = shape.RenderedGeometry; - Transform postMeasureTransform = postMeasureGeometry.Transform; - - var finalSize = new Size(100, 50); - var finalRect = new Rect(finalSize); - shape.Arrange(finalRect); - - Geometry postArrangeGeometry = shape.RenderedGeometry; - Transform postArrangeTransform = postArrangeGeometry.Transform; - - Assert.Equal(postMeasureGeometry, postArrangeGeometry); - Assert.Equal(postMeasureTransform, postArrangeTransform); - Assert.Equal(finalSize, shape.Bounds.Size); - } - } -} From 41050b67422be08b8b15700f3e99fd5879acb58a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 13 Oct 2020 18:26:34 +0200 Subject: [PATCH 18/20] Added some tests for Rectangle and Ellipse. Because these classes override the base `Shape` layout. --- .../Shapes/EllipseTests.cs | 57 +++++++++++++++++++ .../Shapes/RectangleTests.cs | 56 ++++++++++++++++-- 2 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/Shapes/EllipseTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/EllipseTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/EllipseTests.cs new file mode 100644 index 0000000000..626945894f --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Shapes/EllipseTests.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Shapes +{ + public class EllipseTests + { + [Fact] + public void Measure_Does_Not_Set_RenderedGeometry_Rect() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new Ellipse(); + + target.Measure(new Size(100, 100)); + + var geometry = Assert.IsType(target.RenderedGeometry); + Assert.Equal(default, geometry.Rect); + } + + [Fact] + public void Arrange_Sets_RenderedGeometry_Properties() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new Ellipse(); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var geometry = Assert.IsType(target.RenderedGeometry); + Assert.Equal(new Rect(0, 0, 100, 100), geometry.Rect); + } + + [Fact] + public void Rearranging_Updates_RenderedGeometry_Rect() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new Ellipse(); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var geometry = Assert.IsType(target.RenderedGeometry); + Assert.Equal(new Rect(0, 0, 100, 100), geometry.Rect); + + target.Measure(new Size(200, 200)); + target.Arrange(new Rect(0, 0, 200, 200)); + + geometry = Assert.IsType(target.RenderedGeometry); + Assert.Equal(new Rect(0, 0, 200, 200), geometry.Rect); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs index 0ec73edec0..8d8ce10d4c 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Avalonia.Controls.Shapes; +using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.UnitTests; using Moq; @@ -11,6 +8,53 @@ namespace Avalonia.Controls.UnitTests.Shapes { public class RectangleTests { + [Fact] + public void Measure_Does_Not_Set_RenderedGeometry_Rect() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new Rectangle(); + + target.Measure(new Size(100, 100)); + + var geometry = Assert.IsType(target.RenderedGeometry); + Assert.Equal(Rect.Empty, geometry.Rect); + } + + [Fact] + public void Arrange_Sets_RenderedGeometry_Rect() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new Rectangle(); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var geometry = Assert.IsType(target.RenderedGeometry); + Assert.Equal(new Rect(0, 0, 100, 100), geometry.Rect); + } + + [Fact] + public void Rearranging_Updates_RenderedGeometry_Rect() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var target = new Rectangle(); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var geometry = Assert.IsType(target.RenderedGeometry); + Assert.Equal(new Rect(0, 0, 100, 100), geometry.Rect); + + target.Measure(new Size(200, 200)); + target.Arrange(new Rect(0, 0, 200, 200)); + + geometry = Assert.IsType(target.RenderedGeometry); + Assert.Equal(new Rect(0, 0, 200, 200), geometry.Rect); + } + [Fact] public void Changing_Fill_Brush_Color_Should_Invalidate_Visual() { @@ -21,7 +65,7 @@ namespace Avalonia.Controls.UnitTests.Shapes var root = new TestRoot(target); var renderer = Mock.Get(root.Renderer); - renderer.ResetCalls(); + renderer.Invocations.Clear(); ((SolidColorBrush)target.Fill).Color = Colors.Green; @@ -38,7 +82,7 @@ namespace Avalonia.Controls.UnitTests.Shapes var root = new TestRoot(target); var renderer = Mock.Get(root.Renderer); - renderer.ResetCalls(); + renderer.Invocations.Clear(); ((SolidColorBrush)target.Stroke).Color = Colors.Green; From d96c157231c24cc3a167cfa6be10cc13a92b80be Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 15 Oct 2020 11:40:53 +0200 Subject: [PATCH 19/20] Name tuple values. --- src/Avalonia.Controls/Shapes/Shape.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 28206a398a..cd16ac402d 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -252,7 +252,7 @@ namespace Avalonia.Controls.Shapes return default; } - return CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch).Item1; + return CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch).size; } protected override Size ArrangeOverride(Size finalSize) @@ -275,7 +275,7 @@ namespace Avalonia.Controls.Shapes return Size.Empty; } - internal static (Size, Matrix) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch) + internal static (Size size, Matrix transform) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch) { Size shapeSize = new Size(shapeBounds.Right, shapeBounds.Bottom); Matrix translate = Matrix.Identity; From cf6032d8e4d726c97e78f678f6c7630dc52d6235 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 15 Oct 2020 11:41:36 +0200 Subject: [PATCH 20/20] Discard unused value. --- src/Avalonia.Controls/Shapes/Shape.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index cd16ac402d..0b7595ec9a 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -261,7 +261,7 @@ namespace Avalonia.Controls.Shapes { // This should probably use GetRenderBounds(strokeThickness) but then the calculations // will multiply the stroke thickness as well, which isn't correct. - var (size, transform) = CalculateSizeAndTransform(finalSize, DefiningGeometry.Bounds, Stretch); + var (_, transform) = CalculateSizeAndTransform(finalSize, DefiningGeometry.Bounds, Stretch); if (_transform != transform) {