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/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs
index cb50cba540..22f4e9be1f 100644
--- a/samples/ControlCatalog/App.xaml.cs
+++ b/samples/ControlCatalog/App.xaml.cs
@@ -81,7 +81,7 @@ namespace ControlCatalog
public override void Initialize()
{
- Styles.Insert(0, FluentDark);
+ Styles.Insert(0, FluentLight);
AvaloniaXamlLoader.Load(this);
}
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 790813fda0..bd5beafe29 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -77,8 +77,8 @@
Full Decorations
- Fluent - Dark
Fluent - Light
+ Fluent - Dark
Simple - Light
Simple - Dark
diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs
index b0c205246e..c84f2f06b6 100644
--- a/samples/ControlCatalog/MainView.xaml.cs
+++ b/samples/ControlCatalog/MainView.xaml.cs
@@ -38,10 +38,10 @@ namespace ControlCatalog
switch (themes.SelectedIndex)
{
case 0:
- Application.Current.Styles[0] = App.FluentDark;
+ Application.Current.Styles[0] = App.FluentLight;
break;
case 1:
- Application.Current.Styles[0] = App.FluentLight;
+ Application.Current.Styles[0] = App.FluentDark;
break;
case 2:
Application.Current.Styles[0] = App.DefaultLight;
diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml
index 4a1c196917..d4f72f161a 100644
--- a/samples/ControlCatalog/Pages/TextBlockPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml
@@ -18,8 +18,8 @@
-
-
+
+
diff --git a/samples/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs
index 233160b025..340ccdae19 100644
--- a/samples/RenderDemo/App.xaml.cs
+++ b/samples/RenderDemo/App.xaml.cs
@@ -18,6 +18,10 @@ namespace RenderDemo
// App configuration, used by the entry point and previewer
static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure()
+ .With(new Win32PlatformOptions
+ {
+ OverlayPopups = true,
+ })
.UsePlatformDetect()
.UseReactiveUI()
.LogToDebug();
diff --git a/src/Avalonia.Base/AvaloniaLocator.cs b/src/Avalonia.Base/AvaloniaLocator.cs
index f9bbe38bec..3163d15c1b 100644
--- a/src/Avalonia.Base/AvaloniaLocator.cs
+++ b/src/Avalonia.Base/AvaloniaLocator.cs
@@ -54,6 +54,23 @@ namespace Avalonia
return _locator;
}
+ public AvaloniaLocator ToLazy(Func func) where TImlp : TService
+ {
+ var constructed = false;
+ TImlp instance = default;
+ _locator._registry[typeof (TService)] = () =>
+ {
+ if (!constructed)
+ {
+ instance = func();
+ constructed = true;
+ }
+
+ return instance;
+ };
+ return _locator;
+ }
+
public AvaloniaLocator ToSingleton() where TImpl : class, TService, new()
{
TImpl instance = null;
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index 65233f9230..6645d25b5d 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -113,16 +113,8 @@ namespace Avalonia
/// The binding information.
public IBinding this[IndexerDescriptor binding]
{
- get
- {
- return new IndexerBinding(this, binding.Property, binding.Mode);
- }
-
- set
- {
- var sourceBinding = value as IBinding;
- this.Bind(binding.Property, sourceBinding);
- }
+ get { return new IndexerBinding(this, binding.Property, binding.Mode); }
+ set { this.Bind(binding.Property, value); }
}
public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
index 8fc2a7b77c..3bf6842cd6 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using Avalonia.Utilities;
@@ -11,8 +12,11 @@ namespace Avalonia.Data.Core.Plugins
///
public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
{
+ private readonly Dictionary<(Type, string), PropertyInfo> _propertyLookup =
+ new Dictionary<(Type, string), PropertyInfo>();
+
///
- public bool Match(object obj, string propertyName) => GetPropertyWithName(obj.GetType(), propertyName) != null;
+ public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj.GetType(), propertyName) != null;
///
/// Starts monitoring the value of a property on an object.
@@ -30,7 +34,7 @@ namespace Avalonia.Data.Core.Plugins
reference.TryGetTarget(out object instance);
- var p = GetPropertyWithName(instance.GetType(), propertyName);
+ var p = GetFirstPropertyWithName(instance.GetType(), propertyName);
if (p != null)
{
@@ -44,12 +48,40 @@ namespace Avalonia.Data.Core.Plugins
}
}
- private static PropertyInfo GetPropertyWithName(Type type, string propertyName)
+ private PropertyInfo GetFirstPropertyWithName(Type type, string propertyName)
{
- const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public |
- BindingFlags.Static | BindingFlags.Instance;
+ var key = (type, propertyName);
+
+ if (!_propertyLookup.TryGetValue(key, out PropertyInfo propertyInfo))
+ {
+ propertyInfo = TryFindAndCacheProperty(type, propertyName);
+ }
+
+ return propertyInfo;
+ }
+
+ private PropertyInfo TryFindAndCacheProperty(Type type, string propertyName)
+ {
+ PropertyInfo found = null;
+
+ const BindingFlags bindingFlags =
+ BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
+
+ var properties = type.GetProperties(bindingFlags);
+
+ foreach (PropertyInfo propertyInfo in properties)
+ {
+ if (propertyInfo.Name == propertyName)
+ {
+ found = propertyInfo;
+
+ break;
+ }
+ }
+
+ _propertyLookup.Add((type, propertyName), found);
- return type.GetProperty(propertyName, bindingFlags);
+ return found;
}
private class Accessor : PropertyAccessorBase, IWeakSubscriber
diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs
index e9ea942142..e830717b95 100644
--- a/src/Avalonia.Controls/Calendar/CalendarItem.cs
+++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs
@@ -36,11 +36,7 @@ namespace Avalonia.Controls.Primitives
private Button _headerButton;
private Button _nextButton;
private Button _previousButton;
- private Grid _monthView;
- private Grid _yearView;
private ITemplate _dayTitleTemplate;
- private CalendarButton _lastCalendarButton;
- private CalendarDayButton _lastCalendarDayButton;
private DateTime _currentMonth;
private bool _isMouseLeftButtonDown = false;
@@ -160,38 +156,12 @@ namespace Avalonia.Controls.Primitives
///
/// Gets the Grid that hosts the content when in month mode.
///
- internal Grid MonthView
- {
- get { return _monthView; }
- private set
- {
- if (_monthView != null)
- _monthView.PointerLeave -= MonthView_MouseLeave;
-
- _monthView = value;
-
- if (_monthView != null)
- _monthView.PointerLeave += MonthView_MouseLeave;
- }
- }
+ internal Grid MonthView { get; set; }
///
/// Gets the Grid that hosts the content when in year or decade mode.
///
- internal Grid YearView
- {
- get { return _yearView; }
- private set
- {
- if (_yearView != null)
- _yearView.PointerLeave -= YearView_MouseLeave;
-
- _yearView = value;
-
- if (_yearView != null)
- _yearView.PointerLeave += YearView_MouseLeave;
- }
- }
-
+ internal Grid YearView { get; set; }
+
private void PopulateGrids()
{
if (MonthView != null)
@@ -226,7 +196,6 @@ namespace Avalonia.Controls.Primitives
cell.CalendarDayButtonMouseDown += Cell_MouseLeftButtonDown;
cell.CalendarDayButtonMouseUp += Cell_MouseLeftButtonUp;
cell.PointerEnter += Cell_MouseEnter;
- cell.PointerLeave += Cell_MouseLeave;
cell.Click += Cell_Click;
children.Add(cell);
}
@@ -256,7 +225,6 @@ namespace Avalonia.Controls.Primitives
month.CalendarLeftMouseButtonDown += Month_CalendarButtonMouseDown;
month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp;
month.PointerEnter += Month_MouseEnter;
- month.PointerLeave += Month_MouseLeave;
children.Add(month);
}
}
@@ -937,17 +905,7 @@ namespace Avalonia.Controls.Primitives
}
}
}
- internal void Cell_MouseLeave(object sender, PointerEventArgs e)
- {
- if (_isMouseLeftButtonDown)
- {
- CalendarDayButton b = (CalendarDayButton)sender;
- // The button is in Pressed state. Change the state to normal.
- if (e.Pointer.Captured == b)
- e.Pointer.Capture(null);
- _lastCalendarDayButton = b;
- }
- }
+
internal void Cell_MouseLeftButtonDown(object sender, PointerPressedEventArgs e)
{
if (Owner != null)
@@ -1207,35 +1165,6 @@ namespace Avalonia.Controls.Primitives
}
}
- private void Month_MouseLeave(object sender, PointerEventArgs e)
- {
- if (_isMouseLeftButtonDownYearView)
- {
- CalendarButton b = (CalendarButton)sender;
- // The button is in Pressed state. Change the state to normal.
- if (e.Pointer.Captured == b)
- e.Pointer.Capture(null);
- //b.ReleaseMouseCapture();
-
- _lastCalendarButton = b;
- }
- }
- private void MonthView_MouseLeave(object sender, PointerEventArgs e)
- {
- if (_lastCalendarDayButton != null)
- {
- e.Pointer.Capture(_lastCalendarDayButton);
- }
- }
-
- private void YearView_MouseLeave(object sender, PointerEventArgs e)
- {
- if (_lastCalendarButton != null)
- {
- e.Pointer.Capture(_lastCalendarButton);
- }
- }
-
internal void UpdateDisabled(bool isEnabled)
{
PseudoClasses.Set(":calendardisabled", !isEnabled);
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/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs
index 3d8ab3ae48..7d4fef009d 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -101,7 +101,7 @@ namespace Avalonia.Controls
private ICommand? _command;
private bool _commandCanExecute = true;
- private Popup _popup;
+ private Popup? _popup;
///
/// Initializes static members of the class.
@@ -145,7 +145,7 @@ namespace Avalonia.Controls
{
var parent = x as Control;
return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ??
- Observable.Return(null);
+ Observable.Return(null);
});
this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope);
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/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
index 6d6398bcda..a54d1ce308 100644
--- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
+++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
@@ -148,6 +148,7 @@ namespace Avalonia.Controls.Platform
{
case Key.Up:
case Key.Down:
+ {
if (item?.IsTopLevel == true)
{
if (item.HasSubMenu && !item.IsSubMenuOpen)
@@ -161,8 +162,10 @@ namespace Avalonia.Controls.Platform
goto default;
}
break;
+ }
case Key.Left:
+ {
if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen)
{
parent.Close();
@@ -174,8 +177,10 @@ namespace Avalonia.Controls.Platform
goto default;
}
break;
+ }
case Key.Right:
+ {
if (item != null && !item.IsTopLevel && item.HasSubMenu)
{
Open(item, true);
@@ -186,8 +191,10 @@ namespace Avalonia.Controls.Platform
goto default;
}
break;
+ }
case Key.Enter:
+ {
if (item != null)
{
if (!item.HasSubMenu)
@@ -202,12 +209,14 @@ namespace Avalonia.Controls.Platform
e.Handled = true;
}
break;
+ }
case Key.Escape:
- if (item?.Parent != null)
+ {
+ if (item?.Parent is IMenuElement parent)
{
- item.Parent.Close();
- item.Parent.Focus();
+ parent.Close();
+ parent.Focus();
}
else
{
@@ -216,8 +225,10 @@ namespace Avalonia.Controls.Platform
e.Handled = true;
break;
+ }
default:
+ {
var direction = e.Key.ToNavigationDirection();
if (direction.HasValue)
@@ -246,6 +257,7 @@ namespace Avalonia.Controls.Platform
}
break;
+ }
}
if (!e.Handled && item?.Parent is IMenuItem parentItem)
diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index 8837901816..3fd927afa3 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
@@ -1,9 +1,9 @@
using System;
+
+using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
-using Avalonia.Data;
-using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
@@ -14,6 +14,7 @@ namespace Avalonia.Controls.Presenters
///
/// Presents a single item of data inside a template.
///
+ [PseudoClasses(":empty")]
public class ContentPresenter : Control, IContentPresenter
{
///
@@ -102,6 +103,11 @@ namespace Avalonia.Controls.Presenters
TemplatedParentProperty.Changed.AddClassHandler((x, e) => x.TemplatedParentChanged(e));
}
+ public ContentPresenter()
+ {
+ UpdatePseudoClasses();
+ }
+
///
/// Gets or sets a brush with which to paint the background.
///
@@ -424,9 +430,15 @@ namespace Avalonia.Controls.Presenters
_recyclingDataTemplate = null;
}
+ UpdatePseudoClasses();
InvalidateMeasure();
}
+ private void UpdatePseudoClasses()
+ {
+ PseudoClasses.Set(":empty", Content is null);
+ }
+
private double GetLayoutScale()
{
var result = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0;
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/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index f5115a2f7c..6a6d37605d 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -77,7 +77,8 @@ namespace Avalonia.Controls.Presenters
static TextPresenter()
{
- AffectsRender(SelectionBrushProperty);
+ AffectsRender(SelectionBrushProperty, TextBlock.ForegroundProperty,
+ SelectionForegroundBrushProperty, CaretBrushProperty);
AffectsMeasure(TextProperty, PasswordCharProperty, RevealPasswordProperty,
TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index 1e5e80d144..becb489557 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -358,7 +358,7 @@ namespace Avalonia.Controls.Primitives
return;
}
- var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType().FirstOrDefault();
+ var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType();
if (placementTarget == null)
{
@@ -586,6 +586,26 @@ namespace Avalonia.Controls.Primitives
}
Closed?.Invoke(this, EventArgs.Empty);
+
+ var focusCheck = FocusManager.Instance?.Current;
+
+ // Focus is set to null as part of popup closing, so we only want to
+ // set focus to PlacementTarget if this is the case
+ if (focusCheck == null)
+ {
+ if (PlacementTarget != null)
+ {
+ FocusManager.Instance?.Focus(PlacementTarget);
+ }
+ else
+ {
+ var anc = this.FindLogicalAncestorOfType();
+ if (anc != null)
+ {
+ FocusManager.Instance?.Focus(anc);
+ }
+ }
+ }
}
private void ListenForNonClientClick(RawInputEventArgs e)
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/TreeView.cs b/src/Avalonia.Controls/TreeView.cs
index b4c30e0149..b2bd5ab2e5 100644
--- a/src/Avalonia.Controls/TreeView.cs
+++ b/src/Avalonia.Controls/TreeView.cs
@@ -117,10 +117,8 @@ namespace Avalonia.Controls
if (value != null)
{
if (selectedItems.Count != 1 || selectedItems[0] != value)
- {
- _syncingSelectedItems = true;
- SelectSingleItem(value);
- _syncingSelectedItems = false;
+ {
+ SelectSingleItem(value);
}
}
else if (SelectedItems.Count > 0)
@@ -219,8 +217,12 @@ namespace Avalonia.Controls
private void SelectSingleItem(object item)
{
- SelectedItems.Clear();
+ _syncingSelectedItems = true;
+ SelectedItems.Clear();
SelectedItems.Add(item);
+ _syncingSelectedItems = false;
+
+ SetAndRaise(SelectedItemProperty, ref _selectedItem, item);
}
///
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index 4f6af0a41b..6a78f4c6e7 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -238,7 +238,7 @@ namespace Avalonia.Headless
}
}
- class HeadlessBitmapStub : IBitmapImpl, IRenderTargetBitmapImpl, IWriteableBitmapImpl
+ class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl
{
public Size Size { get; }
@@ -267,6 +267,13 @@ namespace Avalonia.Headless
return new HeadlessDrawingContextStub();
}
+ public void Blit(IDrawingContextImpl context)
+ {
+
+ }
+
+ public bool CanBlit => false;
+
public Vector Dpi { get; }
public PixelSize PixelSize { get; }
public int Version { get; set; }
@@ -307,7 +314,7 @@ namespace Avalonia.Headless
}
- public IRenderTargetBitmapImpl CreateLayer(Size size)
+ public IDrawingContextLayerImpl CreateLayer(Size size)
{
return new HeadlessBitmapStub(size, new Vector(96, 96));
}
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.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs
index 5365354418..249b4d547f 100644
--- a/src/Avalonia.OpenGL/Egl/EglContext.cs
+++ b/src/Avalonia.OpenGL/Egl/EglContext.cs
@@ -73,7 +73,8 @@ namespace Avalonia.OpenGL.Egl
var old = new RestoreContext(_egl, _disp.Handle, _lock);
var surf = surface ?? OffscreenSurface;
_egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
- if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context))
+ if (!_egl.MakeCurrent(_disp.Handle, surf?.DangerousGetHandle() ?? IntPtr.Zero,
+ surf?.DangerousGetHandle() ?? IntPtr.Zero, Context))
throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl);
success = true;
return old;
diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs
index fd3de854f5..623364866b 100644
--- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs
+++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs
@@ -158,15 +158,21 @@ namespace Avalonia.OpenGL.Egl
var ctx = _egl.CreateContext(_display, _config, shareCtx?.Context ?? IntPtr.Zero, _contextAttributes);
if (ctx == IntPtr.Zero)
throw OpenGlException.GetFormattedException("eglCreateContext", _egl);
- var surf = _egl.CreatePBufferSurface(_display, _config, new[]
+
+ var extensions = _egl.QueryString(Handle, EGL_EXTENSIONS);
+
+ IntPtr surf = IntPtr.Zero;
+ if (extensions?.Contains("EGL_KHR_surfaceless_context") != true)
{
- EGL_WIDTH, 1,
- EGL_HEIGHT, 1,
- EGL_NONE
- });
- if (surf == IntPtr.Zero)
- throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl);
- var rv = new EglContext(this, _egl, shareCtx, ctx, context => new EglSurface(this, context, surf),
+ surf = _egl.CreatePBufferSurface(_display, _config,
+ new[] { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE });
+ if (surf == IntPtr.Zero)
+ throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl);
+ }
+
+ var rv = new EglContext(this, _egl, shareCtx, ctx,
+ context =>
+ surf == IntPtr.Zero ? null : new EglSurface(this, context, surf),
_version, _sampleCount, _stencilSize);
return rv;
}
diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs
index ea2fe0a99c..28b62136da 100644
--- a/src/Avalonia.OpenGL/GlInterface.cs
+++ b/src/Avalonia.OpenGL/GlInterface.cs
@@ -117,6 +117,19 @@ namespace Avalonia.OpenGL
public delegate int GlCheckFramebufferStatus(int target);
[GlEntryPoint("glCheckFramebufferStatus")]
public GlCheckFramebufferStatus CheckFramebufferStatus { get; }
+
+ public delegate void GlBlitFramebuffer(int srcX0,
+ int srcY0,
+ int srcX1,
+ int srcY1,
+ int dstX0,
+ int dstY0,
+ int dstX1,
+ int dstY1,
+ int mask,
+ int filter);
+ [GlMinVersionEntryPoint("glBlitFramebuffer", 3, 0)]
+ public GlBlitFramebuffer BlitFramebuffer { get; }
public delegate void GlGenRenderbuffers(int count, int[] res);
[GlEntryPoint("glGenRenderbuffers")]
diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml
index 9ce4da0873..9d1c024eb9 100644
--- a/src/Avalonia.Themes.Default/ToggleSwitch.xaml
+++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml
@@ -87,7 +87,6 @@
Grid.Row="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
- Margin="{DynamicResource ToggleSwitchTopHeaderMargin}"
VerticalAlignment="Top"/>
+
+
+
+
+
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/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml
index 88c6b661f1..902fc74c0c 100644
--- a/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml
+++ b/src/Avalonia.Themes.Fluent/DataValidationErrors.xaml
@@ -31,12 +31,16 @@
-
@@ -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 @@
+
+
+
+
+