diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 721a0415f4..e67fa14c57 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -31,6 +31,8 @@ jobs:
condition: not(canceled())
- job: macOS
+ variables:
+ SolutionDir: '$(Build.SourcesDirectory)'
pool:
vmImage: 'macOS-10.14'
steps:
@@ -97,6 +99,8 @@ jobs:
- job: Windows
pool:
vmImage: 'windows-2019'
+ variables:
+ SolutionDir: '$(Build.SourcesDirectory)'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.401'
diff --git a/build/ApiDiff.props b/build/ApiDiff.props
index da82fbcc51..3d322f56d5 100644
--- a/build/ApiDiff.props
+++ b/build/ApiDiff.props
@@ -7,6 +7,6 @@
-
+
diff --git a/build/Assets/Icon.png b/build/Assets/Icon.png
new file mode 100644
index 0000000000..41a2a618fb
Binary files /dev/null and b/build/Assets/Icon.png differ
diff --git a/build/SharedVersion.props b/build/SharedVersion.props
index d3cebef418..bc76058f41 100644
--- a/build/SharedVersion.props
+++ b/build/SharedVersion.props
@@ -10,10 +10,14 @@
CS1591
latest
MIT
- https://avatars2.githubusercontent.com/u/14075148?s=200
+ Icon.png
Avalonia is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), MacOS and with experimental support for Android and iOS.
avalonia;avaloniaui;mvvm;rx;reactive extensions;android;ios;mac;forms;wpf;net;netstandard;net461;uwp;xamarin
https://github.com/AvaloniaUI/Avalonia/releases
git
+
+
+
+
diff --git a/build/SourceLink.props b/build/SourceLink.props
index 0c9b6a34f8..e27727c9e8 100644
--- a/build/SourceLink.props
+++ b/build/SourceLink.props
@@ -1,5 +1,5 @@
-
+
-
\ No newline at end of file
+
diff --git a/dirs.proj b/dirs.proj
index bf32abef72..594f2c22d3 100644
--- a/dirs.proj
+++ b/dirs.proj
@@ -21,6 +21,7 @@
+
diff --git a/samples/BindingDemo/App.xaml.cs b/samples/BindingDemo/App.xaml.cs
index 13875aeb21..5c38ab8305 100644
--- a/samples/BindingDemo/App.xaml.cs
+++ b/samples/BindingDemo/App.xaml.cs
@@ -1,6 +1,5 @@
-using System;
using Avalonia;
-using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
@@ -13,13 +12,20 @@ namespace BindingDemo
AvaloniaXamlLoader.Load(this);
}
- private static void Main()
+ public override void OnFrameworkInitializationCompleted()
{
- AppBuilder.Configure()
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ desktop.MainWindow = new MainWindow();
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ public static int Main(string[] args)
+ => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
.UsePlatformDetect()
.UseReactiveUI()
- .LogToDebug()
- .Start();
- }
+ .LogToDebug();
}
}
diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs
index b2df1953f5..e0cc8cc904 100644
--- a/samples/ControlCatalog.Desktop/Program.cs
+++ b/samples/ControlCatalog.Desktop/Program.cs
@@ -10,12 +10,8 @@ namespace ControlCatalog
internal class Program
{
[STAThread]
- static void Main(string[] args)
- {
- // TODO: Make this work with GTK/Skia/Cairo depending on command-line args
- // again.
- BuildAvaloniaApp().Start();
- }
+ public static int Main(string[] args)
+ => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
///
/// This method is needed for IDE previewer infrastructure
diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
index 28b0257eda..2752703e21 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -11,7 +11,6 @@
-
diff --git a/samples/Previewer/App.xaml.cs b/samples/Previewer/App.xaml.cs
index fffa987a27..ab83d45cd3 100644
--- a/samples/Previewer/App.xaml.cs
+++ b/samples/Previewer/App.xaml.cs
@@ -1,4 +1,5 @@
using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace Previewer
@@ -9,6 +10,13 @@ namespace Previewer
{
AvaloniaXamlLoader.Load(this);
}
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ desktop.MainWindow = new MainWindow();
+ base.OnFrameworkInitializationCompleted();
+ }
}
-}
\ No newline at end of file
+}
diff --git a/samples/Previewer/Program.cs b/samples/Previewer/Program.cs
index 48363e27f2..b12b93974a 100644
--- a/samples/Previewer/Program.cs
+++ b/samples/Previewer/Program.cs
@@ -1,13 +1,14 @@
-using System;
-using Avalonia;
+using Avalonia;
namespace Previewer
{
class Program
{
- static void Main(string[] args)
- {
- AppBuilder.Configure().UsePlatformDetect().Start();
- }
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect();
+
+ public static int Main(string[] args)
+ => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
-}
\ No newline at end of file
+}
diff --git a/samples/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs
index 340ccdae19..2247fd7c5a 100644
--- a/samples/RenderDemo/App.xaml.cs
+++ b/samples/RenderDemo/App.xaml.cs
@@ -1,4 +1,5 @@
using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
@@ -11,9 +12,17 @@ namespace RenderDemo
AvaloniaXamlLoader.Load(this);
}
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ desktop.MainWindow = new MainWindow();
+ base.OnFrameworkInitializationCompleted();
+ }
+
// TODO: Make this work with GTK/Skia/Cairo depending on command-line args
// again.
- static void Main(string[] args) => BuildAvaloniaApp().Start();
+ static void Main(string[] args)
+ => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
// App configuration, used by the entry point and previewer
static AppBuilder BuildAvaloniaApp()
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml
index fb3e318a0e..c2914e8847 100644
--- a/samples/RenderDemo/Pages/GlyphRunPage.xaml
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml
@@ -6,9 +6,9 @@
x:Class="RenderDemo.Pages.GlyphRunPage">
-
-
+
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
index ddee880288..857358f6b2 100644
--- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
@@ -9,7 +9,7 @@ namespace RenderDemo.Pages
{
public class GlyphRunPage : UserControl
{
- private DrawingPresenter _drawingPresenter;
+ private Image _imageControl;
private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
private readonly Random _rand = new Random();
private ushort[] _glyphIndices = new ushort[1];
@@ -25,7 +25,8 @@ namespace RenderDemo.Pages
{
AvaloniaXamlLoader.Load(this);
- _drawingPresenter = this.FindControl("drawingPresenter");
+ _imageControl = this.FindControl("imageControl");
+ _imageControl.Source = new DrawingImage();
DispatcherTimer.Run(() =>
{
@@ -73,7 +74,7 @@ namespace RenderDemo.Pages
drawingGroup.Children.Add(geometryDrawing);
- _drawingPresenter.Drawing = drawingGroup;
+ (_imageControl.Source as DrawingImage).Drawing = drawingGroup;
}
}
}
diff --git a/samples/VirtualizationDemo/App.xaml.cs b/samples/VirtualizationDemo/App.xaml.cs
index 5990dd282c..81b80c1f40 100644
--- a/samples/VirtualizationDemo/App.xaml.cs
+++ b/samples/VirtualizationDemo/App.xaml.cs
@@ -1,4 +1,5 @@
using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace VirtualizationDemo
@@ -9,5 +10,12 @@ namespace VirtualizationDemo
{
AvaloniaXamlLoader.Load(this);
}
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ desktop.MainWindow = new MainWindow();
+ base.OnFrameworkInitializationCompleted();
+ }
}
}
diff --git a/samples/VirtualizationDemo/Program.cs b/samples/VirtualizationDemo/Program.cs
index 93ea5e1b88..c23bfd7ad9 100644
--- a/samples/VirtualizationDemo/Program.cs
+++ b/samples/VirtualizationDemo/Program.cs
@@ -1,19 +1,17 @@
-using System;
-using Avalonia;
-using Avalonia.Controls;
+using Avalonia;
using Avalonia.ReactiveUI;
namespace VirtualizationDemo
{
class Program
{
- static void Main(string[] args)
- {
- AppBuilder.Configure()
- .UsePlatformDetect()
- .UseReactiveUI()
- .LogToDebug()
- .Start();
- }
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .UseReactiveUI()
+ .LogToDebug();
+
+ public static int Main(string[] args)
+ => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
}
diff --git a/samples/interop/Direct3DInteropSample/App.paml.cs b/samples/interop/Direct3DInteropSample/App.paml.cs
index 1b6d5fd39c..29365decfe 100644
--- a/samples/interop/Direct3DInteropSample/App.paml.cs
+++ b/samples/interop/Direct3DInteropSample/App.paml.cs
@@ -1,4 +1,5 @@
using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace Direct3DInteropSample
@@ -9,5 +10,12 @@ namespace Direct3DInteropSample
{
AvaloniaXamlLoader.Load(this);
}
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ desktop.MainWindow = new MainWindow();
+ base.OnFrameworkInitializationCompleted();
+ }
}
}
diff --git a/samples/interop/Direct3DInteropSample/Program.cs b/samples/interop/Direct3DInteropSample/Program.cs
index 21302fa68a..bf8e76d7e4 100644
--- a/samples/interop/Direct3DInteropSample/Program.cs
+++ b/samples/interop/Direct3DInteropSample/Program.cs
@@ -1,19 +1,16 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Avalonia;
+using Avalonia;
namespace Direct3DInteropSample
{
class Program
{
- static void Main(string[] args)
- {
- AppBuilder.Configure()
- .With(new Win32PlatformOptions {UseDeferredRendering = false})
- .UseWin32().UseDirect2D1().Start();
- }
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .With(new Win32PlatformOptions { UseDeferredRendering = false })
+ .UseWin32()
+ .UseDirect2D1();
+
+ public static int Main(string[] args)
+ => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
}
diff --git a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj
index cc831ef8ae..0e0e71fbb2 100644
--- a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj
+++ b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj
@@ -9,7 +9,6 @@
-
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
index 23c4acdf6c..92ddd4e736 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
@@ -665,6 +665,7 @@ namespace Avalonia.Controls
///
/// The data item represented by the row that contains the intended cell.
///
+ /// When the method returns, contains the applied binding.
///
/// A new editing element that is bound to the column's property value.
///
diff --git a/src/Avalonia.Controls/IScrollAnchorProvider.cs b/src/Avalonia.Controls/IScrollAnchorProvider.cs
index 93f3a0abb8..7ba02e99ea 100644
--- a/src/Avalonia.Controls/IScrollAnchorProvider.cs
+++ b/src/Avalonia.Controls/IScrollAnchorProvider.cs
@@ -1,4 +1,6 @@
-namespace Avalonia.Controls
+#nullable enable
+
+namespace Avalonia.Controls
{
///
/// Specifies a contract for a scrolling control that supports scroll anchoring.
@@ -8,7 +10,7 @@
///
/// The currently chosen anchor element to use for scroll anchoring.
///
- IControl CurrentAnchor { get; }
+ IControl? CurrentAnchor { get; }
///
/// Registers a control as a potential scroll anchor candidate.
diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs
index ccb92dc497..b7eeb065da 100644
--- a/src/Avalonia.Controls/Panel.cs
+++ b/src/Avalonia.Controls/Panel.cs
@@ -137,6 +137,11 @@ namespace Avalonia.Controls
throw new NotSupportedException();
}
+ InvalidateMeasureOnChildrenChanged();
+ }
+
+ private protected virtual void InvalidateMeasureOnChildrenChanged()
+ {
InvalidateMeasure();
}
diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
index 3fac440c40..bdc68bee7e 100644
--- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
@@ -512,6 +512,14 @@ namespace Avalonia.Controls.Presenters
var generator = Owner.ItemContainerGenerator;
var newOffset = -1.0;
+ if (!panel.IsMeasureValid && panel.PreviousMeasure.HasValue)
+ {
+ //before any kind of scrolling we need to make sure panel measure is valid
+ //or we risk get panel into not valid state
+ //we make a preemptive quick measure so scrolling is valid
+ panel.Measure(panel.PreviousMeasure.Value);
+ }
+
if (index >= 0 && index < ItemCount)
{
if (index <= FirstIndex)
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 5fcb14c858..b0b52812b9 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -7,6 +7,8 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.VisualTree;
+#nullable enable
+
namespace Avalonia.Controls.Presenters
{
///
@@ -14,6 +16,8 @@ namespace Avalonia.Controls.Presenters
///
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
{
+ private const double EdgeDetectionTolerance = 0.1;
+
///
/// Defines the property.
///
@@ -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 _activeLogicalGestureScrolls;
- private List _anchorCandidates;
- private (IControl control, Rect bounds) _anchor;
+ private Dictionary? _activeLogicalGestureScrolls;
+ private List? _anchorCandidates;
+ private IControl? _anchorElement;
+ private Rect _anchorElementBounds;
+ private bool _isAnchorElementDirty;
///
/// Initializes static members of the class.
@@ -90,8 +96,6 @@ namespace Avalonia.Controls.Presenters
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
}
- internal event EventHandler PreArrange;
-
///
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
///
@@ -138,7 +142,14 @@ namespace Avalonia.Controls.Presenters
}
///
- IControl IScrollAnchorProvider.CurrentAnchor => _anchor.control;
+ IControl? IScrollAnchorProvider.CurrentAnchor
+ {
+ get
+ {
+ EnsureAnchorElementSelection();
+ return _anchorElement;
+ }
+ }
///
/// Attempts to bring a portion of the target visual into view by scrolling the content.
@@ -215,16 +226,18 @@ namespace Avalonia.Controls.Presenters
_anchorCandidates ??= new List();
_anchorCandidates.Add(element);
+ _isAnchorElementDirty = true;
}
///
void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
{
_anchorCandidates?.Remove(element);
+ _isAnchorElementDirty = true;
- if (_anchor.control == element)
+ if (_anchorElement == element)
{
- _anchor = default;
+ _anchorElement = null;
}
}
@@ -247,11 +260,6 @@ namespace Avalonia.Controls.Presenters
///
protected override Size ArrangeOverride(Size finalSize)
{
- PreArrange?.Invoke(this, new VectorEventArgs
- {
- Vector = new Vector(finalSize.Width, finalSize.Height),
- });
-
if (_logicalScrollSubscription != null || Child == null)
{
return base.ArrangeOverride(finalSize);
@@ -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)
diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs
index 82a49c4189..ab81fe869e 100644
--- a/src/Avalonia.Controls/Primitives/IPopupHost.cs
+++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs
@@ -47,6 +47,7 @@ namespace Avalonia.Controls.Primitives
/// The offset, in device-independent pixels.
/// The anchor point.
/// The popup gravity.
+ /// Defines how a popup position will be adjusted if the unadjusted position would result in the popup being partly constrained.
///
/// The anchor rect. If null, the bounds of will be used.
///
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index 4317d795f1..280f46be9f 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -354,19 +354,15 @@ namespace Avalonia.Controls.Primitives
///
/// The control that raised the event.
/// The container or null if the event did not originate in a container.
- protected IControl? GetContainerFromEventSource(IInteractive eventSource)
+ protected IControl? GetContainerFromEventSource(IInteractive? eventSource)
{
- var parent = (IVisual)eventSource;
-
- while (parent != null)
+ for (var current = eventSource as IVisual; current != null; current = current.VisualParent)
{
- if (parent is IControl control && control.LogicalParent == this
- && ItemContainerGenerator?.IndexFromContainer(control) != -1)
+ if (current is IControl control && control.LogicalParent == this &&
+ ItemContainerGenerator?.IndexFromContainer(control) != -1)
{
return control;
}
-
- parent = parent.VisualParent;
}
return null;
@@ -670,7 +666,7 @@ namespace Avalonia.Controls.Primitives
/// false.
///
protected bool UpdateSelectionFromEventSource(
- IInteractive eventSource,
+ IInteractive? eventSource,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false,
@@ -794,18 +790,13 @@ namespace Avalonia.Controls.Primitives
/// The event.
private void ContainerSelectionChanged(RoutedEventArgs e)
{
- if (!_ignoreContainerSelectionChanged)
+ if (!_ignoreContainerSelectionChanged &&
+ e.Source is IControl control &&
+ e.Source is ISelectable selectable &&
+ control.LogicalParent == this &&
+ ItemContainerGenerator?.IndexFromContainer(control) != -1)
{
- var control = e.Source as IControl;
- var selectable = e.Source as ISelectable;
-
- if (control != null &&
- selectable != null &&
- control.LogicalParent == this &&
- ItemContainerGenerator?.IndexFromContainer(control) != -1)
- {
- UpdateSelection(control, selectable.IsSelected);
- }
+ UpdateSelection(control, selectable.IsSelected);
}
if (e.Source != this)
@@ -824,12 +815,11 @@ namespace Avalonia.Controls.Primitives
{
try
{
- var selectable = container as ISelectable;
bool result;
_ignoreContainerSelectionChanged = true;
- if (selectable != null)
+ if (container is ISelectable selectable)
{
result = selectable.IsSelected;
selectable.IsSelected = selected;
diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs
index f96ca9310d..6b2c566422 100644
--- a/src/Avalonia.Controls/Primitives/ToggleButton.cs
+++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs
@@ -94,7 +94,7 @@ namespace Avalonia.Controls.Primitives
set
{
SetAndRaise(IsCheckedProperty, ref _isChecked, value);
- UpdatePseudoClasses(value);
+ UpdatePseudoClasses(IsChecked);
}
}
diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
index 40f1b8dbb9..fb2da09e73 100644
--- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
@@ -267,6 +267,11 @@ namespace Avalonia.Controls
return result;
}
+ private protected override void InvalidateMeasureOnChildrenChanged()
+ {
+ // Don't invalidate measure when children change.
+ }
+
protected override Size MeasureOverride(Size availableSize)
{
if (_isLayoutInProgress)
@@ -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);
diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs
index 416b1e2824..cf2066b373 100644
--- a/src/Avalonia.Controls/Repeater/ViewManager.cs
+++ b/src/Avalonia.Controls/Repeater/ViewManager.cs
@@ -661,7 +661,7 @@ namespace Avalonia.Controls
children.Add(element);
}
- repeater.OnElementPrepared(element, index);
+ repeater.OnElementPrepared(element, virtInfo);
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs
index bdb0fa3270..6e24408aa9 100644
--- a/src/Avalonia.Controls/Repeater/ViewportManager.cs
+++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs
@@ -240,9 +240,14 @@ namespace Avalonia.Controls
}
}
- public void OnElementPrepared(IControl element)
+ public void OnElementPrepared(IControl element, VirtualizationInfo virtInfo)
{
- _scroller?.RegisterAnchorCandidate(element);
+ // WinUI registers the element as an anchor candidate here, but I feel that's in error:
+ // at this point the element has not yet been positioned by the arrange pass so it will
+ // have its previous position, meaning that when the arrange pass moves it into its new
+ // position, an incorrect scroll anchoring will occur. Instead signal that it's not yet
+ // registered as a scroll anchor candidate.
+ virtInfo.IsRegisteredAsAnchorCandidate = false;
}
public void OnElementCleared(IControl element)
@@ -373,6 +378,11 @@ namespace Avalonia.Controls
}
}
+ public void RegisterScrollAnchorCandidate(IControl element)
+ {
+ _scroller?.RegisterAnchorCandidate(element);
+ }
+
private IControl GetImmediateChildOfRepeater(IControl descendant)
{
var targetChild = descendant;
diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
index 7a639419c1..f8cfde609e 100644
--- a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
+++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
@@ -38,6 +38,7 @@ namespace Avalonia.Controls
public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool;
public bool MustClearDataContext { get; set; }
public bool KeepAlive { get; set; }
+ public bool IsRegisteredAsAnchorCandidate { get; set; }
public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory;
public string UniqueId { get; private set; }
diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs
index 7d1525afc4..0b7595ec9a 100644
--- a/src/Avalonia.Controls/Shapes/Shape.cs
+++ b/src/Avalonia.Controls/Shapes/Shape.cs
@@ -62,7 +62,6 @@ namespace Avalonia.Controls.Shapes
private Matrix _transform = Matrix.Identity;
private Geometry? _definingGeometry;
private Geometry? _renderedGeometry;
- private bool _calculateTransformOnArrange;
static Shape()
{
@@ -248,52 +247,21 @@ namespace Avalonia.Controls.Shapes
protected override Size MeasureOverride(Size availableSize)
{
- bool deferCalculateTransform;
- switch (Stretch)
+ if (DefiningGeometry is null)
{
- case Stretch.Fill:
- case Stretch.UniformToFill:
- deferCalculateTransform = double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height);
- break;
- case Stretch.Uniform:
- deferCalculateTransform = double.IsInfinity(availableSize.Width) && double.IsInfinity(availableSize.Height);
- break;
- case Stretch.None:
- default:
- deferCalculateTransform = false;
- break;
+ return default;
}
- if (deferCalculateTransform)
- {
- _calculateTransformOnArrange = true;
- return DefiningGeometry?.Bounds.Size ?? Size.Empty;
- }
- else
- {
- _calculateTransformOnArrange = false;
- return CalculateShapeSizeAndSetTransform(availableSize);
- }
+ return CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch).size;
}
protected override Size ArrangeOverride(Size finalSize)
- {
- if (_calculateTransformOnArrange)
- {
- _calculateTransformOnArrange = false;
- CalculateShapeSizeAndSetTransform(finalSize);
- }
-
- return finalSize;
- }
-
- private Size CalculateShapeSizeAndSetTransform(Size availableSize)
{
if (DefiningGeometry != null)
{
// This should probably use GetRenderBounds(strokeThickness) but then the calculations
// will multiply the stroke thickness as well, which isn't correct.
- var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch);
+ var (_, transform) = CalculateSizeAndTransform(finalSize, DefiningGeometry.Bounds, Stretch);
if (_transform != transform)
{
@@ -301,13 +269,13 @@ namespace Avalonia.Controls.Shapes
_renderedGeometry = null;
}
- return size;
+ return finalSize;
}
return Size.Empty;
}
- internal static (Size, Matrix) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch)
+ internal static (Size size, Matrix transform) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch)
{
Size shapeSize = new Size(shapeBounds.Right, shapeBounds.Bottom);
Matrix translate = Matrix.Identity;
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index 317b6d3f2e..54574a7e1c 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -451,7 +451,7 @@ namespace Avalonia.Controls
///
/// The dialog result.
///
- /// When the window is shown with the
+ /// When the window is shown with the
/// or method, the
/// resulting task will produce the value when the window
/// is closed.
diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs
index aa6fcc8bff..e155666631 100644
--- a/src/Avalonia.Input/KeyGesture.cs
+++ b/src/Avalonia.Input/KeyGesture.cs
@@ -144,7 +144,10 @@ namespace Avalonia.Input
return s.ToString();
}
- public bool Matches(KeyEventArgs keyEvent) => ResolveNumPadOperationKey(keyEvent.Key) == Key && keyEvent.KeyModifiers == KeyModifiers;
+ public bool Matches(KeyEventArgs keyEvent) =>
+ keyEvent != null &&
+ keyEvent.KeyModifiers == KeyModifiers &&
+ ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(Key);
// TODO: Move that to external key parser
private static Key ParseKey(string key)
diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs
index 909c7bc7eb..4a93c8344f 100644
--- a/src/Avalonia.Layout/StackLayout.cs
+++ b/src/Avalonia.Layout/StackLayout.cs
@@ -249,8 +249,8 @@ namespace Avalonia.Layout
realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize)
{
anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize);
- offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent);
anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex));
+ offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent);
}
}
diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml
index 8522c933ae..573b9f337f 100644
--- a/src/Avalonia.Themes.Fluent/Button.xaml
+++ b/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 @@
+
+
diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml
index 678ae5c5a3..83d2779872 100644
--- a/src/Avalonia.Themes.Fluent/CheckBox.xaml
+++ b/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}" />
+ Width="20" />
@@ -52,6 +50,14 @@
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml
index 078f51c87a..2847d1fa5a 100644
--- a/src/Avalonia.Themes.Fluent/RadioButton.xaml
+++ b/src/Avalonia.Themes.Fluent/RadioButton.xaml
@@ -25,8 +25,7 @@
+ BorderThickness="{TemplateBinding BorderThickness}">
@@ -77,6 +76,10 @@
+
+
diff --git a/src/Avalonia.Themes.Fluent/TabItem.xaml b/src/Avalonia.Themes.Fluent/TabItem.xaml
index 2b0a0c1ea0..1c9574f169 100644
--- a/src/Avalonia.Themes.Fluent/TabItem.xaml
+++ b/src/Avalonia.Themes.Fluent/TabItem.xaml
@@ -39,7 +39,6 @@
TextBlock.FontSize="{TemplateBinding FontSize}"
TextBlock.FontWeight="{TemplateBinding FontWeight}" />
@@ -53,6 +52,7 @@
diff --git a/src/Avalonia.Themes.Fluent/TabStripItem.xaml b/src/Avalonia.Themes.Fluent/TabStripItem.xaml
index 628ab8dddd..78ef102705 100644
--- a/src/Avalonia.Themes.Fluent/TabStripItem.xaml
+++ b/src/Avalonia.Themes.Fluent/TabStripItem.xaml
@@ -38,7 +38,6 @@
TextBlock.FontSize="{TemplateBinding FontSize}"
TextBlock.FontWeight="{TemplateBinding FontWeight}" />
@@ -46,6 +45,9 @@
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/ToggleButton.xaml
index 49e2280a6d..dd8e51e4e5 100644
--- a/src/Avalonia.Themes.Fluent/ToggleButton.xaml
+++ b/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 @@
+
+