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 @@ - - + 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 @@ + + + + +