Browse Source

Merge branch 'master' into fixes/4822-shape-arrange

pull/4858/head
Dariusz Komosiński 6 years ago
committed by GitHub
parent
commit
108ed67691
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. 2
      src/Avalonia.Layout/StackLayout.cs
  11. 4
      src/Avalonia.Visuals/Media/GlyphRun.cs
  12. 8
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  13. 36
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  14. 160
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  15. 47
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  16. 22
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

6
src/Avalonia.Controls/IScrollAnchorProvider.cs

@ -1,4 +1,6 @@
namespace Avalonia.Controls #nullable enable
namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Specifies a contract for a scrolling control that supports scroll anchoring. /// Specifies a contract for a scrolling control that supports scroll anchoring.
@ -8,7 +10,7 @@
/// <summary> /// <summary>
/// The currently chosen anchor element to use for scroll anchoring. /// The currently chosen anchor element to use for scroll anchoring.
/// </summary> /// </summary>
IControl CurrentAnchor { get; } IControl? CurrentAnchor { get; }
/// <summary> /// <summary>
/// Registers a control as a potential scroll anchor candidate. /// 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(); throw new NotSupportedException();
} }
InvalidateMeasureOnChildrenChanged();
}
private protected virtual void InvalidateMeasureOnChildrenChanged()
{
InvalidateMeasure(); InvalidateMeasure();
} }

8
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -512,6 +512,14 @@ namespace Avalonia.Controls.Presenters
var generator = Owner.ItemContainerGenerator; var generator = Owner.ItemContainerGenerator;
var newOffset = -1.0; 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 >= 0 && index < ItemCount)
{ {
if (index <= FirstIndex) if (index <= FirstIndex)

137
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -7,6 +7,8 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.VisualTree; using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
/// <summary> /// <summary>
@ -14,6 +16,8 @@ namespace Avalonia.Controls.Presenters
/// </summary> /// </summary>
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
{ {
private const double EdgeDetectionTolerance = 0.1;
/// <summary> /// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property. /// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary> /// </summary>
@ -64,11 +68,13 @@ namespace Avalonia.Controls.Presenters
private bool _arranging; private bool _arranging;
private Size _extent; private Size _extent;
private Vector _offset; private Vector _offset;
private IDisposable _logicalScrollSubscription; private IDisposable? _logicalScrollSubscription;
private Size _viewport; private Size _viewport;
private Dictionary<int, Vector> _activeLogicalGestureScrolls; private Dictionary<int, Vector>? _activeLogicalGestureScrolls;
private List<IControl> _anchorCandidates; private List<IControl>? _anchorCandidates;
private (IControl control, Rect bounds) _anchor; private IControl? _anchorElement;
private Rect _anchorElementBounds;
private bool _isAnchorElementDirty;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class. /// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -90,8 +96,6 @@ namespace Avalonia.Controls.Presenters
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
} }
internal event EventHandler<VectorEventArgs> PreArrange;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally. /// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary> /// </summary>
@ -138,7 +142,14 @@ namespace Avalonia.Controls.Presenters
} }
/// <inheritdoc/> /// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => _anchor.control; IControl? IScrollAnchorProvider.CurrentAnchor
{
get
{
EnsureAnchorElementSelection();
return _anchorElement;
}
}
/// <summary> /// <summary>
/// Attempts to bring a portion of the target visual into view by scrolling the content. /// 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 ??= new List<IControl>();
_anchorCandidates.Add(element); _anchorCandidates.Add(element);
_isAnchorElementDirty = true;
} }
/// <inheritdoc/> /// <inheritdoc/>
void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element) void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
{ {
_anchorCandidates?.Remove(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/> /// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
PreArrange?.Invoke(this, new VectorEventArgs
{
Vector = new Vector(finalSize.Width, finalSize.Height),
});
if (_logicalScrollSubscription != null || Child == null) if (_logicalScrollSubscription != null || Child == null)
{ {
return base.ArrangeOverride(finalSize); 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 // 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 // arrange then that change wasn't just due to scrolling (as scrolling doesn't adjust
// relative positions within Child). // relative positions within Child).
if (_anchor.control != null && if (_anchorElement != null &&
TranslateBounds(_anchor.control, Child, out var updatedBounds) && TranslateBounds(_anchorElement, Child, out var updatedBounds) &&
updatedBounds.Position != _anchor.bounds.Position) updatedBounds.Position != _anchorElementBounds.Position)
{ {
var offset = updatedBounds.Position - _anchor.bounds.Position; var offset = updatedBounds.Position - _anchorElementBounds.Position;
return offset; return offset;
} }
return default; return default;
} }
// Calculate the new anchor element. var isAnchoring = Offset.X >= EdgeDetectionTolerance || Offset.Y >= EdgeDetectionTolerance;
_anchor = CalculateCurrentAnchor();
// Do the arrange. if (isAnchoring)
ArrangeOverrideImpl(size, -Offset); {
// 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. // Do the arrange.
var anchorShift = TrackAnchor(); ArrangeOverrideImpl(size, -Offset);
if (anchorShift != default) // If the anchor moved during the arrange, we need to adjust the offset and do another arrange.
{ var anchorShift = TrackAnchor();
var newOffset = Offset + anchorShift;
var newExtent = Extent;
var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height);
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) if (newOffset.X > maxOffset.X)
{ {
newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height); newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width);
} }
Extent = newExtent; if (newOffset.Y > maxOffset.Y)
{
newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height);
}
try Extent = newExtent;
{
_arranging = true; try
Offset = newOffset; {
} _arranging = true;
finally Offset = newOffset;
{ }
_arranging = false; finally
{
_arranging = false;
}
ArrangeOverrideImpl(size, -Offset);
} }
}
else
{
ArrangeOverrideImpl(size, -Offset); ArrangeOverrideImpl(size, -Offset);
} }
Viewport = finalSize; Viewport = finalSize;
Extent = Child.Bounds.Size.Inflate(Child.Margin); Extent = Child.Bounds.Size.Inflate(Child.Margin);
_isAnchorElementDirty = true;
return finalSize; return finalSize;
} }
@ -350,7 +368,7 @@ namespace Avalonia.Controls.Presenters
{ {
var logicalUnits = delta.Y / LogicalScrollItemSize; var logicalUnits = delta.Y / LogicalScrollItemSize;
delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize); delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize);
dy = logicalUnits * scrollable.ScrollSize.Height; dy = logicalUnits * scrollable!.ScrollSize.Height;
} }
else else
dy = delta.Y; dy = delta.Y;
@ -368,7 +386,7 @@ namespace Avalonia.Controls.Presenters
{ {
var logicalUnits = delta.X / LogicalScrollItemSize; var logicalUnits = delta.X / LogicalScrollItemSize;
delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize); delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize);
dx = logicalUnits * scrollable.ScrollSize.Width; dx = logicalUnits * scrollable!.ScrollSize.Width;
} }
else else
dx = delta.X; dx = delta.X;
@ -405,7 +423,7 @@ namespace Avalonia.Controls.Presenters
if (Extent.Height > Viewport.Height) 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 += -e.Delta.Y * height;
y = Math.Max(y, 0); y = Math.Max(y, 0);
y = Math.Min(y, Extent.Height - Viewport.Height); y = Math.Min(y, Extent.Height - Viewport.Height);
@ -413,7 +431,7 @@ namespace Avalonia.Controls.Presenters
if (Extent.Width > Viewport.Width) 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 += -e.Delta.X * width;
x = Math.Max(x, 0); x = Math.Max(x, 0);
x = Math.Min(x, Extent.Width - Viewport.Width); x = Math.Min(x, Extent.Width - Viewport.Width);
@ -441,7 +459,7 @@ namespace Avalonia.Controls.Presenters
private void ChildChanged(AvaloniaPropertyChangedEventArgs e) private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
{ {
UpdateScrollableSubscription((IControl)e.NewValue); UpdateScrollableSubscription((IControl?)e.NewValue);
if (e.OldValue != null) 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; 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 bestCandidate = default(IControl);
var bestCandidateDistance = double.MaxValue; var bestCandidateDistance = double.MaxValue;
@ -531,10 +553,9 @@ namespace Avalonia.Controls.Presenters
// bounds aren't relative to the ScrollContentPresenter itself, if they change // bounds aren't relative to the ScrollContentPresenter itself, if they change
// then we know it wasn't just due to scrolling. // then we know it wasn't just due to scrolling.
var unscrolledBounds = TranslateBounds(bestCandidate, Child); var unscrolledBounds = TranslateBounds(bestCandidate, Child);
return (bestCandidate, unscrolledBounds); _anchorElement = bestCandidate;
_anchorElementBounds = unscrolledBounds;
} }
return default;
} }
private bool GetViewportBounds(IControl element, out Rect bounds) private bool GetViewportBounds(IControl element, out Rect bounds)

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

@ -94,7 +94,7 @@ namespace Avalonia.Controls.Primitives
set set
{ {
SetAndRaise(IsCheckedProperty, ref _isChecked, value); 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; return result;
} }
private protected override void InvalidateMeasureOnChildrenChanged()
{
// Don't invalidate measure when children change.
}
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
if (_isLayoutInProgress) if (_isLayoutInProgress)
@ -364,6 +369,12 @@ namespace Avalonia.Controls
{ {
var newBounds = element.Bounds; var newBounds = element.Bounds;
virtInfo.ArrangeBounds = newBounds; virtInfo.ArrangeBounds = newBounds;
if (!virtInfo.IsRegisteredAsAnchorCandidate)
{
_viewportManager.RegisterScrollAnchorCandidate(element);
virtInfo.IsRegisteredAsAnchorCandidate = true;
}
} }
} }
@ -515,11 +526,14 @@ namespace Avalonia.Controls
return element; 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) if (ElementPrepared != null)
{ {
var index = virtInfo.Index;
if (_elementPreparedArgs == null) if (_elementPreparedArgs == null)
{ {
_elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index); _elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index);

2
src/Avalonia.Controls/Repeater/ViewManager.cs

@ -661,7 +661,7 @@ namespace Avalonia.Controls
children.Add(element); children.Add(element);
} }
repeater.OnElementPrepared(element, index); repeater.OnElementPrepared(element, virtInfo);
// Update realized indices // Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); _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) 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) private IControl GetImmediateChildOfRepeater(IControl descendant)
{ {
var targetChild = 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 IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool;
public bool MustClearDataContext { get; set; } public bool MustClearDataContext { get; set; }
public bool KeepAlive { get; set; } public bool KeepAlive { get; set; }
public bool IsRegisteredAsAnchorCandidate { get; set; }
public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory; public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory;
public string UniqueId { get; private set; } public string UniqueId { get; private set; }

2
src/Avalonia.Layout/StackLayout.cs

@ -249,8 +249,8 @@ namespace Avalonia.Layout
realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize) realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize)
{ {
anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize); anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize);
offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent);
anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex)); 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]) if (characterIndex > GlyphClusters[GlyphClusters.Length - 1])
{ {
return _glyphClusters.End; return _glyphClusters.Length - 1;
} }
} }
else else
{ {
if (characterIndex < GlyphClusters[GlyphClusters.Length - 1]) if (characterIndex < GlyphClusters[GlyphClusters.Length - 1])
{ {
return _glyphClusters.End; return _glyphClusters.Length - 1;
} }
if (characterIndex > GlyphClusters[0]) if (characterIndex > GlyphClusters[0])

8
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@ -339,14 +339,6 @@ namespace Avalonia.Media.TextFormatting
return true; 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; return true;
} }

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

@ -238,22 +238,46 @@ namespace Avalonia.Direct2D1
width = 0; 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]; return new GlyphRunImpl(run);
width += run.Advances[i];
} }
run.Offsets = new GlyphOffset[glyphCount]; run.Offsets = new GlyphOffset[glyphCount];
for (var i = 0; i < glyphCount; i++) for (var i = 0; i < glyphCount; i++)
{ {
var offset = glyphRun.GlyphOffsets[i]; var (x, y) = glyphRun.GlyphOffsets[i];
run.Offsets[i] = new GlyphOffset run.Offsets[i] = new GlyphOffset
{ {
AdvanceOffset = (float)offset.X, AdvanceOffset = (float)x,
AscenderOffset = (float)offset.Y 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;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -15,51 +15,9 @@ namespace Avalonia.Direct2D1.Media
{ {
using (var buffer = new Buffer()) using (var buffer = new Buffer())
{ {
buffer.ContentType = ContentType.Unicode; FillBuffer(buffer, text);
var breakCharPosition = text.Length - 1; buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
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.GuessSegmentProperties(); buffer.GuessSegmentProperties();
@ -67,44 +25,38 @@ namespace Avalonia.Direct2D1.Media
var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font;
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
font.Shape(buffer); font.Shape(buffer);
font.GetScale(out var scaleX, out _); font.GetScale(out var scaleX, out _);
var textScale = fontRenderingEmSize / scaleX; 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; glyphIndices[i] = (ushort)glyphInfos[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;
glyphAdvances[i] = advanceX; clusters[i] = (ushort)glyphInfos[i].Cluster;
var offsetX = pos[i].XOffset * textScale; if (!glyphTypeface.IsFixedPitch)
var offsetY = pos[i].YOffset * textScale; {
SetAdvance(glyphPositions, i, textScale, ref glyphAdvances);
}
glyphOffsets[i] = new Vector(offsetX, offsetY); SetOffset(glyphPositions, i, textScale, ref glyphOffsets);
} }
return new GlyphRun(glyphTypeface, fontRenderingEmSize, return new GlyphRun(glyphTypeface, fontRenderingEmSize,
@ -115,5 +67,79 @@ namespace Avalonia.Direct2D1.Media
new ReadOnlySlice<ushort>(clusters)); 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;
}
} }
} }

47
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -407,6 +407,53 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, raised); 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() private FuncControlTemplate ListBoxTemplate()
{ {
return new FuncControlTemplate<ListBox>((parent, scope) => return new FuncControlTemplate<ListBox>((parent, scope) =>

22
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -417,7 +417,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
[Theory] [Theory]
[InlineData("abcde\r\n", 7)] // Carriage Return + Line Feed [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\u000A", 6)] // Line Feed
[InlineData("abcde\u000B", 6)] // Vertical Tab [InlineData("abcde\u000B", 6)] // Vertical Tab
[InlineData("abcde\u000C", 6)] // Form Feed [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] [Fact]
public void Should_Wrap_Min_OneCharacter_EveryLine() public void Should_Wrap_Min_OneCharacter_EveryLine()
{ {

Loading…
Cancel
Save