Browse Source

Merge branch 'master' into fix-custom-titlebar-hittest

pull/4857/head
Dariusz Komosiński 5 years ago
committed by GitHub
parent
commit
bb03c0b84e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      src/Avalonia.Controls/IScrollAnchorProvider.cs
  2. 5
      src/Avalonia.Controls/Panel.cs
  3. 8
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  4. 137
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  5. 2
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  6. 18
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  7. 2
      src/Avalonia.Controls/Repeater/ViewManager.cs
  8. 14
      src/Avalonia.Controls/Repeater/ViewportManager.cs
  9. 1
      src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
  10. 44
      src/Avalonia.Controls/Shapes/Shape.cs
  11. 2
      src/Avalonia.Layout/StackLayout.cs
  12. 4
      src/Avalonia.Visuals/Media/GlyphRun.cs
  13. 8
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  14. 36
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  15. 160
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  16. 9
      tests/Avalonia.Controls.UnitTests/CanvasTests.cs
  17. 15
      tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs
  18. 47
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  19. 6
      tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs
  20. 57
      tests/Avalonia.Controls.UnitTests/Shapes/EllipseTests.cs
  21. 144
      tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs
  22. 56
      tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs
  23. 13
      tests/Avalonia.Controls.UnitTests/ViewboxTests.cs
  24. 113
      tests/Avalonia.Layout.UnitTests/ShapeLayoutTests.cs
  25. 22
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  26. 2
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

6
src/Avalonia.Controls/IScrollAnchorProvider.cs

@ -1,4 +1,6 @@
namespace Avalonia.Controls
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Specifies a contract for a scrolling control that supports scroll anchoring.
@ -8,7 +10,7 @@
/// <summary>
/// The currently chosen anchor element to use for scroll anchoring.
/// </summary>
IControl CurrentAnchor { get; }
IControl? CurrentAnchor { get; }
/// <summary>
/// Registers a control as a potential scroll anchor candidate.

5
src/Avalonia.Controls/Panel.cs

@ -137,6 +137,11 @@ namespace Avalonia.Controls
throw new NotSupportedException();
}
InvalidateMeasureOnChildrenChanged();
}
private protected virtual void InvalidateMeasureOnChildrenChanged()
{
InvalidateMeasure();
}

8
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)

137
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
{
/// <summary>
@ -14,6 +16,8 @@ namespace Avalonia.Controls.Presenters
/// </summary>
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
{
private const double EdgeDetectionTolerance = 0.1;
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
@ -64,11 +68,13 @@ namespace Avalonia.Controls.Presenters
private bool _arranging;
private Size _extent;
private Vector _offset;
private IDisposable _logicalScrollSubscription;
private IDisposable? _logicalScrollSubscription;
private Size _viewport;
private Dictionary<int, Vector> _activeLogicalGestureScrolls;
private List<IControl> _anchorCandidates;
private (IControl control, Rect bounds) _anchor;
private Dictionary<int, Vector>? _activeLogicalGestureScrolls;
private List<IControl>? _anchorCandidates;
private IControl? _anchorElement;
private Rect _anchorElementBounds;
private bool _isAnchorElementDirty;
/// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -90,8 +96,6 @@ namespace Avalonia.Controls.Presenters
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
}
internal event EventHandler<VectorEventArgs> PreArrange;
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
@ -138,7 +142,14 @@ namespace Avalonia.Controls.Presenters
}
/// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => _anchor.control;
IControl? IScrollAnchorProvider.CurrentAnchor
{
get
{
EnsureAnchorElementSelection();
return _anchorElement;
}
}
/// <summary>
/// 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<IControl>();
_anchorCandidates.Add(element);
_isAnchorElementDirty = true;
}
/// <inheritdoc/>
void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
{
_anchorCandidates?.Remove(element);
_isAnchorElementDirty = true;
if (_anchor.control == element)
if (_anchorElement == element)
{
_anchor = default;
_anchorElement = null;
}
}
@ -247,11 +260,6 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
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);
@ -271,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;
}
@ -350,7 +368,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 +386,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 +423,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 +431,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 +459,7 @@ namespace Avalonia.Controls.Presenters
private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdateScrollableSubscription((IControl)e.NewValue);
UpdateScrollableSubscription((IControl?)e.NewValue);
if (e.OldValue != null)
{
@ -449,7 +467,7 @@ namespace Avalonia.Controls.Presenters
}
}
private void UpdateScrollableSubscription(IControl child)
private void UpdateScrollableSubscription(IControl? child)
{
var scrollable = child as ILogicalScrollable;
@ -498,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;
@ -531,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)

2
src/Avalonia.Controls/Primitives/ToggleButton.cs

@ -94,7 +94,7 @@ namespace Avalonia.Controls.Primitives
set
{
SetAndRaise(IsCheckedProperty, ref _isChecked, value);
UpdatePseudoClasses(value);
UpdatePseudoClasses(IsChecked);
}
}

18
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)
@ -364,6 +369,12 @@ namespace Avalonia.Controls
{
var newBounds = element.Bounds;
virtInfo.ArrangeBounds = newBounds;
if (!virtInfo.IsRegisteredAsAnchorCandidate)
{
_viewportManager.RegisterScrollAnchorCandidate(element);
virtInfo.IsRegisteredAsAnchorCandidate = true;
}
}
}
@ -515,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);

2
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);

14
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;

1
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; }

44
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()
{
@ -248,52 +247,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).size;
}
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 (_, transform) = CalculateSizeAndTransform(finalSize, DefiningGeometry.Bounds, Stretch);
if (_transform != transform)
{
@ -301,13 +269,13 @@ namespace Avalonia.Controls.Shapes
_renderedGeometry = null;
}
return size;
return finalSize;
}
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;

2
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);
}
}

4
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])

8
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;
}

36
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -238,22 +238,46 @@ namespace Avalonia.Direct2D1
width = 0;
for (var i = 0; i < glyphCount; i++)
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]) * scale;
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
};
}

160
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<ushort>(clusters));
}
}
private static void FillBuffer(Buffer buffer, ReadOnlySlice<char> 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<GlyphPosition> 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<GlyphPosition> 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;
}
}
}

9
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
{

15
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;

47
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<string>();
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<object>((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<ListBox>((parent, scope) =>

6
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 };

57
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<EllipseGeometry>(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<EllipseGeometry>(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<EllipseGeometry>(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<EllipseGeometry>(target.RenderedGeometry);
Assert.Equal(new Rect(0, 0, 200, 200), geometry.Rect);
}
}
}

144
tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs

@ -34,5 +34,149 @@ 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 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()
{
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()
{
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);
}
}
}

56
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<RectangleGeometry>(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<RectangleGeometry>(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<RectangleGeometry>(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<RectangleGeometry>(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;

13
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));

113
tests/Avalonia.Layout.UnitTests/ShapeLayoutTests.cs

@ -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<IPlatformRenderInterface>().ToSingleton<MockPlatformRenderInterface>();
}
[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<Point>
{
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<Point>
{
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<Point>
{
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);
}
}
}

22
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()
{

2
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -34,7 +34,7 @@ namespace Avalonia.UnitTests
public IGeometryImpl CreateRectangleGeometry(Rect rect)
{
return Mock.Of<IGeometryImpl>();
return Mock.Of<IGeometryImpl>(x => x.Bounds == rect);
}
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)

Loading…
Cancel
Save