Browse Source

Merge branch 'master' into fixes/FontMetrics

pull/4863/head
Benedikt Stebner 6 years ago
committed by GitHub
parent
commit
d953b3f8ed
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. 5
      src/Avalonia.Themes.Fluent/Button.xaml
  12. 14
      src/Avalonia.Themes.Fluent/CheckBox.xaml
  13. 12
      src/Avalonia.Themes.Fluent/ComboBox.xaml
  14. 7
      src/Avalonia.Themes.Fluent/RadioButton.xaml
  15. 2
      src/Avalonia.Themes.Fluent/TabItem.xaml
  16. 4
      src/Avalonia.Themes.Fluent/TabStripItem.xaml
  17. 5
      src/Avalonia.Themes.Fluent/TextBox.xaml
  18. 5
      src/Avalonia.Themes.Fluent/ToggleButton.xaml
  19. 4
      src/Avalonia.Visuals/Media/GlyphRun.cs
  20. 8
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  21. 36
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  22. 160
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  23. 47
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  24. 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>
/// 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; }

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

5
src/Avalonia.Themes.Fluent/Button.xaml

@ -32,7 +32,6 @@
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
CornerRadius="{DynamicResource ControlCornerRadius}"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
@ -95,4 +94,8 @@
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" />
<Setter Property="TextBlock.Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />
</Style>
<Style Selector="Button /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
</Style>
</Styles>

14
src/Avalonia.Themes.Fluent/CheckBox.xaml

@ -22,16 +22,14 @@
Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{DynamicResource ControlCornerRadius}" />
BorderThickness="{TemplateBinding BorderThickness}" />
<Grid VerticalAlignment="Top" Height="32">
<Border x:Name="NormalRectangle"
BorderThickness="{DynamicResource CheckBoxBorderThemeThickness}"
UseLayoutRounding="False"
Height="20"
Width="20"
CornerRadius="{DynamicResource ControlCornerRadius}" />
Width="20" />
<Viewbox UseLayoutRounding="False">
<Panel>
@ -52,6 +50,14 @@
</ControlTemplate>
</Setter>
</Style>
<Style Selector="CheckBox /template/ Border#PART_Border">
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
</Style>
<Style Selector="CheckBox /template/ Border#NormalRectangle">
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
</Style>
<!-- Unchecked Normal State -->
<Style Selector="CheckBox">

12
src/Avalonia.Themes.Fluent/ComboBox.xaml

@ -61,7 +61,6 @@
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{DynamicResource ControlCornerRadius}"
MinWidth="{DynamicResource ComboBoxThemeMinWidth}" />
<Border x:Name="HighlightBackground"
@ -70,8 +69,7 @@
Grid.ColumnSpan="2"
Background="{DynamicResource ComboBoxBackgroundUnfocused}"
BorderBrush="{DynamicResource ComboBoxBackgroundBorderBrushUnfocused}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{DynamicResource ControlCornerRadius}" />
BorderThickness="{TemplateBinding BorderThickness}" />
<TextBlock x:Name="PlaceholderTextBlock"
Grid.Row="1"
Grid.Column="0"
@ -226,4 +224,12 @@
<Style Selector="ComboBox:focused:pressed /template/ Path#DropDownGlyph">
<Setter Property="Fill" Value="{DynamicResource ComboBoxDropDownGlyphForegroundFocusedPressed}" />
</Style>
<Style Selector="ComboBox /template/ Border#Background">
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
</Style>
<Style Selector="ComboBox /template/ Border#HighlightBackground">
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
</Style>
</Styles>

7
src/Avalonia.Themes.Fluent/RadioButton.xaml

@ -25,8 +25,7 @@
<Border Name="RootBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{DynamicResource ControlCornerRadius}">
BorderThickness="{TemplateBinding BorderThickness}">
<Grid ColumnDefinitions="20,*">
<Grid VerticalAlignment="Top"
Height="32">
@ -77,6 +76,10 @@
<Setter Property="Stroke" Value="{DynamicResource RadioButtonCheckGlyphStroke}" />
<Setter Property="Fill" Value="{DynamicResource RadioButtonCheckGlyphFill}" />
</Style>
<Style Selector="RadioButton /template/ Border#RootBorder">
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
</Style>
<!-- PointerOver State -->

2
src/Avalonia.Themes.Fluent/TabItem.xaml

@ -39,7 +39,6 @@
TextBlock.FontSize="{TemplateBinding FontSize}"
TextBlock.FontWeight="{TemplateBinding FontWeight}" />
<Border Name="PART_SelectedPipe"
CornerRadius="{DynamicResource ControlCornerRadius}"
Background="{DynamicResource TabItemHeaderSelectedPipeFill}" />
</Panel>
</Border>
@ -53,6 +52,7 @@
</Style>
<Style Selector="TabItem /template/ Border#PART_SelectedPipe">
<Setter Property="IsVisible" Value="False" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
</Style>
<!-- Selected state -->

4
src/Avalonia.Themes.Fluent/TabStripItem.xaml

@ -38,7 +38,6 @@
TextBlock.FontSize="{TemplateBinding FontSize}"
TextBlock.FontWeight="{TemplateBinding FontWeight}" />
<Border Name="PART_SelectedPipe"
CornerRadius="{DynamicResource ControlCornerRadius}"
Background="{DynamicResource TabItemHeaderSelectedPipeFill}" />
</Panel>
</Border>
@ -46,6 +45,9 @@
</Setter>
</Style>
<Style Selector="TabStripItem /template/ Border#PART_SelectedPipe">
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
</Style>
<!-- Nornal state -->
<Style Selector="TabStripItem /template/ Border#PART_LayoutRoot">

5
src/Avalonia.Themes.Fluent/TextBox.xaml

@ -52,7 +52,6 @@
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{DynamicResource ControlCornerRadius}"
MinWidth="{TemplateBinding MinWidth}"
MinHeight="{TemplateBinding MinHeight}">
</Border>
@ -156,6 +155,10 @@
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlErrorTextForegroundBrush}"/>
</Style>
<Style Selector="TextBox /template/ Border#PART_BorderElement">
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
</Style>
<Style Selector="TextBox /template/ DockPanel#PART_InnerDockPanel">
<Setter Property="Cursor" Value="IBeam" />
</Style>

5
src/Avalonia.Themes.Fluent/ToggleButton.xaml

@ -29,7 +29,6 @@
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{DynamicResource ControlCornerRadius}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Padding="{TemplateBinding Padding}"
@ -38,6 +37,10 @@
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ToggleButton /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
</Style>
<Style Selector="ToggleButton:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundPointerOver}" />

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

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

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

Loading…
Cancel
Save