Browse Source

Merge branch 'fixes/FontMetrics' of https://github.com/Gillibald/Avalonia into fixes/FontMetrics

pull/4863/head
Benedikt Schroeder 6 years ago
parent
commit
c4dfdfab67
  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> /// <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);
} }
} }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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