Browse Source

Merge branch 'master' into fixes/macCaretPosition

Benedikt Stebner 2 years ago
parent
commit
9b741c8998
74 changed files with 866 additions and 363 deletions
  1. 7 3
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  2. 49 11
      samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
  3. 4 4
      samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs
  4. 6 0
      samples/IntegrationTestApp/MainWindow.axaml
  5. 5 1
      samples/IntegrationTestApp/MainWindow.axaml.cs
  6. 18 6
      samples/IntegrationTestApp/ShowWindowTest.axaml
  7. 1 1
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  8. 1 1
      samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
  9. 0 9
      src/Avalonia.Base/CornerRadius.cs
  10. 1 1
      src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs
  11. 1 10
      src/Avalonia.Base/Media/BoxShadow.cs
  12. 2 2
      src/Avalonia.Base/Media/BoxShadows.cs
  13. 1 1
      src/Avalonia.Base/Media/DrawingGroup.cs
  14. 0 5
      src/Avalonia.Base/Media/FontFamily.cs
  15. 3 2
      src/Avalonia.Base/Media/FormattedText.cs
  16. 1 1
      src/Avalonia.Base/Media/ImageDrawing.cs
  17. 1 1
      src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs
  18. 2 17
      src/Avalonia.Base/PixelRect.cs
  19. 16 1
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  20. 13 3
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  21. 0 8
      src/Avalonia.Base/Point.cs
  22. 8 26
      src/Avalonia.Base/Rect.cs
  23. 6 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  24. 25 30
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  25. 1 1
      src/Avalonia.Base/Rendering/DirtyRects.cs
  26. 2 2
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  27. 0 11
      src/Avalonia.Base/Size.cs
  28. 0 10
      src/Avalonia.Base/Thickness.cs
  29. 0 8
      src/Avalonia.Base/Vector.cs
  30. 4 4
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  31. 2 2
      src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs
  32. 1 1
      src/Avalonia.Controls/BorderVisual.cs
  33. 20 0
      src/Avalonia.Controls/ContainerClearingEventArgs.cs
  34. 32 0
      src/Avalonia.Controls/ContainerIndexChangedEventArgs.cs
  35. 26 0
      src/Avalonia.Controls/ContainerPreparedEventArgs.cs
  36. 45 58
      src/Avalonia.Controls/ContextMenu.cs
  37. 1 1
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  38. 1 1
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  39. 7 9
      src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
  40. 31 0
      src/Avalonia.Controls/ItemsControl.cs
  41. 2 2
      src/Avalonia.Controls/LayoutTransformControl.cs
  42. 1 1
      src/Avalonia.Controls/NativeControlHost.cs
  43. 1 2
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  44. 47 33
      src/Avalonia.Controls/Primitives/Popup.cs
  45. 2 1
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  46. 1 1
      src/Avalonia.Controls/SelectableTextBlock.cs
  47. 1 1
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  48. 0 1
      src/Avalonia.Controls/TopLevel.cs
  49. 3 2
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  50. 1 1
      src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
  51. 1 1
      src/Avalonia.Themes.Fluent/Controls/Menu.xaml
  52. 1 1
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  53. 1 1
      src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
  54. 1 1
      src/Avalonia.Themes.Simple/Controls/DatePicker.xaml
  55. 1 1
      src/Avalonia.Themes.Simple/Controls/Menu.xaml
  56. 1 1
      src/Avalonia.Themes.Simple/Controls/MenuItem.xaml
  57. 1 1
      src/Avalonia.Themes.Simple/Controls/TimePicker.xaml
  58. 2 2
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  59. 1 1
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  60. 130 0
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  61. 140 0
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  62. 1 1
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  63. 10 10
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  64. 30 0
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
  65. 13 6
      tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs
  66. 11 2
      tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs
  67. 9 2
      tests/Avalonia.IntegrationTests.Appium/PlatformTheoryAttribute.cs
  68. 102 22
      tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs
  69. 2 2
      tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs
  70. 1 1
      tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs
  71. 1 1
      tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
  72. 1 1
      tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
  73. 1 1
      tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
  74. 1 1
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

+ 7 - 3
native/Avalonia.Native/src/OSX/WindowImpl.mm

@@ -281,10 +281,13 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) {
 
             case SystemDecorationsFull:
                 [Window setHasShadow:YES];
-                [Window setTitleVisibility:NSWindowTitleVisible];
-                [Window setTitlebarAppearsTransparent:NO];
                 [Window setTitle:_lastTitle];
 
+                if (!_isClientAreaExtended) {
+                    [Window setTitleVisibility:NSWindowTitleVisible];
+                    [Window setTitlebarAppearsTransparent:NO];
+                }
+
                 if (currentWindowState == Maximized) {
                     auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
 
@@ -611,7 +614,8 @@ void WindowImpl::UpdateStyle() {
     }
 
     bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
-    bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull;
+    bool hasTrafficLights = (_decorations == SystemDecorationsFull) &&
+        (_isClientAreaExtended ? wantsChrome : true);
     
     NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton];
     NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton];

+ 49 - 11
samples/ControlCatalog/Pages/ClipboardPage.xaml.cs

@@ -1,16 +1,23 @@
 using System;
 using System.Collections.Generic;
-
+using System.Linq;
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.Notifications;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
+using Avalonia.Platform;
+using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
 
 namespace ControlCatalog.Pages
 {
     public partial class ClipboardPage : UserControl
     {
+        private INotificationManager? _notificationManager;
+        private INotificationManager NotificationManager => _notificationManager
+            ??= new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
         public ClipboardPage()
         {
             InitializeComponent();
@@ -31,7 +38,7 @@ namespace ControlCatalog.Pages
 
         private async void PasteText(object? sender, RoutedEventArgs args)
         {
-            if(Application.Current!.Clipboard is { } clipboard)
+            if (Application.Current!.Clipboard is { } clipboard)
             {
                 ClipboardContent.Text = await clipboard.GetTextAsync();
             }
@@ -59,15 +66,45 @@ namespace ControlCatalog.Pages
         {
             if (Application.Current!.Clipboard is { } clipboard)
             {
-                var files = (ClipboardContent.Text ?? String.Empty)
-                .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
-                if (files.Length == 0)
+                var storageProvider = TopLevel.GetTopLevel(this)!.StorageProvider;
+                var filesPath = (ClipboardContent.Text ?? string.Empty)
+                    .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
+                if (filesPath.Length == 0)
                 {
                     return;
                 }
-                var dataObject = new DataObject();
-                dataObject.Set(DataFormats.FileNames, files);
-                await clipboard.SetDataObjectAsync(dataObject);
+                List<string> invalidFile = new(filesPath.Length);
+                List<IStorageFile> files = new(filesPath.Length);
+
+                for (int i = 0; i < filesPath.Length; i++)
+                {
+                    var file = await storageProvider.TryGetFileFromPathAsync(filesPath[i]);
+                    if (file is null)
+                    {
+                        invalidFile.Add(filesPath[i]);
+                    }
+                    else
+                    {
+                        files.Add(file);
+                    }
+                }
+
+                if (invalidFile.Count > 0)
+                {
+                    NotificationManager.Show(new Notification("Warning", "There is one o more invalid path.", NotificationType.Warning));
+                }
+
+                if (files.Count > 0)
+                {
+                    var dataObject = new DataObject();
+                    dataObject.Set(DataFormats.Files, files);
+                    await clipboard.SetDataObjectAsync(dataObject);
+                    NotificationManager.Show(new Notification("Success", "Copy completated.", NotificationType.Success));
+                }
+                else
+                {
+                    NotificationManager.Show(new Notification("Warning", "Any files to copy in Clipboard.", NotificationType.Warning));
+                }
             }
         }
 
@@ -75,8 +112,9 @@ namespace ControlCatalog.Pages
         {
             if (Application.Current!.Clipboard is { } clipboard)
             {
-                var fiels = await clipboard.GetDataAsync(DataFormats.FileNames) as IEnumerable<string>;
-                ClipboardContent.Text = fiels != null ? string.Join(Environment.NewLine, fiels) : string.Empty;
+                var files = await clipboard.GetDataAsync(DataFormats.Files) as IEnumerable<Avalonia.Platform.Storage.IStorageItem>;
+
+                ClipboardContent.Text = files != null ? string.Join(Environment.NewLine, files.Select(f => f.TryGetLocalPath() ?? f.Name)) : string.Empty;
             }
         }
 
@@ -95,7 +133,7 @@ namespace ControlCatalog.Pages
             {
                 await clipboard.ClearAsync();
             }
-                
+
         }
     }
 }

+ 4 - 4
samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs

@@ -133,17 +133,17 @@ namespace ControlCatalog.Pages
 
             // 0,0 refers to the top-left of the control now. It is not prime time to draw gui stuff because it'll be under the world 
 
-            var translateModifier = context.PushPreTransform(Avalonia.Matrix.CreateTranslation(new Avalonia.Vector(halfWidth, halfHeight)));
+            var translateModifier = context.PushTransform(Avalonia.Matrix.CreateTranslation(new Avalonia.Vector(halfWidth, halfHeight)));
 
             // now 0,0 refers to the ViewportCenter(X,Y). 
             var rotationMatrix = Avalonia.Matrix.CreateRotation(Rotation);
-            var rotationModifier = context.PushPreTransform(rotationMatrix);
+            var rotationModifier = context.PushTransform(rotationMatrix);
 
             // everything is rotated but not scaled 
 
-            var scaleModifier = context.PushPreTransform(Avalonia.Matrix.CreateScale(Scale, -Scale));
+            var scaleModifier = context.PushTransform(Avalonia.Matrix.CreateScale(Scale, -Scale));
 
-            var mapPositionModifier = context.PushPreTransform(Matrix.CreateTranslation(new Vector(-ViewportCenterX, -ViewportCenterY)));
+            var mapPositionModifier = context.PushTransform(Matrix.CreateTranslation(new Vector(-ViewportCenterX, -ViewportCenterY)));
 
             // now everything is rotated and scaled, and at the right position, now we're drawing strictly in world coordinates
 

+ 6 - 0
samples/IntegrationTestApp/MainWindow.axaml

@@ -151,6 +151,12 @@
               <ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
               <ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
             </ComboBox>
+            <ComboBox Name="ShowWindowSystemDecorations" SelectedIndex="2">
+              <ComboBoxItem Name="ShowWindowSystemDecorationsNone">None</ComboBoxItem>
+              <ComboBoxItem Name="ShowWindowSystemDecorationsBorderOnly">BorderOnly</ComboBoxItem>
+              <ComboBoxItem Name="ShowWindowSystemDecorationsFull">Full</ComboBoxItem>
+            </ComboBox>
+            <CheckBox Name="ShowWindowExtendClientAreaToDecorationsHint">ExtendClientAreaToDecorationsHint</CheckBox>
             <CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox>
             <Button Name="ShowWindow">Show Window</Button>
             <Button Name="SendToBack">Send to Back</Button>

+ 5 - 1
samples/IntegrationTestApp/MainWindow.axaml.cs

@@ -68,6 +68,8 @@ namespace IntegrationTestApp
             var locationComboBox = this.GetControl<ComboBox>("ShowWindowLocation");
             var stateComboBox = this.GetControl<ComboBox>("ShowWindowState");
             var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null;
+            var systemDecorations = this.GetControl<ComboBox>("ShowWindowSystemDecorations");
+            var extendClientArea = this.GetControl<CheckBox>("ShowWindowExtendClientAreaToDecorationsHint");
             var canResizeCheckBox = this.GetControl<CheckBox>("ShowWindowCanResize");
             var owner = (Window)this.GetVisualRoot()!;
 
@@ -95,6 +97,8 @@ namespace IntegrationTestApp
             }
 
             sizeTextBox.Text = string.Empty;
+            window.ExtendClientAreaToDecorationsHint = extendClientArea.IsChecked ?? false;
+            window.SystemDecorations = (SystemDecorations)systemDecorations.SelectedIndex;
             window.WindowState = (WindowState)stateComboBox.SelectedIndex;
 
             switch (modeComboBox.SelectedIndex)
@@ -158,7 +162,7 @@ namespace IntegrationTestApp
             var popup = new Popup
             {
                 WindowManagerAddShadowHint = false,
-                PlacementMode = PlacementMode.AnchorAndGravity,
+                Placement = PlacementMode.AnchorAndGravity,
                 PlacementAnchor = PopupAnchor.Top,
                 PlacementGravity = PopupGravity.Bottom,
                 Width= 200,

+ 18 - 6
samples/IntegrationTestApp/ShowWindowTest.axaml

@@ -6,7 +6,7 @@
         x:DataType="Window"
         Title="Show Window Test">
   <integrationTestApp:MeasureBorder Name="MyBorder">
-    <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
+    <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
       <Label Grid.Column="0" Grid.Row="1">Client Size</Label>
       <TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
                Text="{Binding ClientSize, Mode=OneWay}" />
@@ -35,13 +35,25 @@
         <ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
       </ComboBox>
 
-      <Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
-      <TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True" />
+      <Label Grid.Column="0" Grid.Row="8">SystemDecorations</Label>
+      <ComboBox Name="CurrentSystemDecorations" Grid.Column="1" Grid.Row="8"  SelectedIndex="{Binding SystemDecorations}">
+        <ComboBoxItem Name="SystemDecorationsNone">None</ComboBoxItem>
+        <ComboBoxItem Name="SystemDecorationsBorderOnly">BorderOnly</ComboBoxItem>
+        <ComboBoxItem Name="SystemDecorationsFull">Full</ComboBoxItem>
+      </ComboBox>
+
+      <CheckBox Name="CurrentExtendClientAreaToDecorationsHint" Grid.ColumnSpan="2" Grid.Row="9"
+                IsChecked="{Binding ExtendClientAreaToDecorationsHint}">
+        ExtendClientAreaToDecorationsHint
+      </CheckBox>
+
+      <Label Grid.Column="0" Grid.Row="10">Order (mac)</Label>
+      <TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="10" IsReadOnly="True" />
       
-      <Label Grid.Row="9" Content="MeasuredWith:" />
-      <TextBlock Grid.Column="1" Grid.Row="9" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
+      <Label Grid.Row="11" Content="MeasuredWith:" />
+      <TextBlock Grid.Column="1" Grid.Row="11" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
 
-      <Button Name="HideButton" Grid.Row="10" Command="{Binding $parent[Window].Hide}">Hide</Button>
+      <Button Name="HideButton" Grid.Row="12" Command="{Binding $parent[Window].Hide}">Hide</Button>
       
     </Grid>
   </integrationTestApp:MeasureBorder>

+ 1 - 1
samples/RenderDemo/Pages/RenderTargetBitmapPage.cs

@@ -29,7 +29,7 @@ namespace RenderDemo.Pages
         public override void Render(DrawingContext context)
         {
             using (var ctx = _bitmap.CreateDrawingContext())
-            using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100)
+            using (ctx.PushTransform(Matrix.CreateTranslation(-100, -100)
                                          * Matrix.CreateRotation(_st.Elapsed.TotalSeconds)
                                          * Matrix.CreateTranslation(100, 100)))
             {

+ 1 - 1
samples/SampleControls/HamburgerMenu/HamburgerMenu.cs

@@ -57,7 +57,7 @@ namespace ControlSamples
             {
                 if (_splitView is not null && _splitView.DisplayMode == SplitViewDisplayMode.Overlay)
                 {
-                    _splitView.SetValue(SplitView.IsPaneOpenProperty, false, Avalonia.Data.BindingPriority.Animation);
+                    _splitView.SetCurrentValue(SplitView.IsPaneOpenProperty, false);
                 }
             }
         }

+ 0 - 9
src/Avalonia.Base/CornerRadius.cs

@@ -60,15 +60,6 @@ namespace Avalonia
         /// </summary>
         public double BottomLeft { get; }
 
-        /// <summary>
-        /// Gets a value indicating whether the instance has default values (all corner radii are set to 0).
-        /// </summary>
-        public bool IsDefault => TopLeft == 0 && TopRight == 0 && BottomLeft == 0 && BottomRight == 0;
-
-        /// <inheritdoc cref="IsDefault"/>
-        [Obsolete("Use IsDefault instead.")]
-        public bool IsEmpty => IsDefault;
-
         /// <summary>
         /// Gets a value indicating whether all corner radii are equal.
         /// </summary>

+ 1 - 1
src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs

@@ -180,7 +180,7 @@ namespace Avalonia.Input.GestureRecognizers
         internal Velocity GetVelocity()
         {
             var estimate = GetVelocityEstimate();
-            if (estimate == null || estimate.PixelsPerSecond.IsDefault)
+            if (estimate == null || estimate.PixelsPerSecond == default(Vector))
             {
                 return new Velocity(Vector.Zero);
             }

+ 1 - 10
src/Avalonia.Base/Media/BoxShadow.cs

@@ -45,15 +45,6 @@ namespace Avalonia.Media
             }
         }
 
-        /// <summary>
-        /// Gets a value indicating whether the instance has default values.
-        /// </summary>
-        public bool IsDefault => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0;
-
-        /// <inheritdoc cref="IsDefault"/>
-        [Obsolete("Use IsDefault instead.")]
-        public bool IsEmpty => IsDefault;
-
         private readonly static char[] s_Separator = new char[] { ' ', '\t' };
 
         struct ArrayReader
@@ -89,7 +80,7 @@ namespace Avalonia.Media
         {
             var sb = StringBuilderCache.Acquire();
 
-            if (IsDefault)
+            if (this == default)
             {
                 return "none";
             }

+ 2 - 2
src/Avalonia.Base/Media/BoxShadows.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Media
         {
             _first = shadow;
             _list = null;
-            Count = _first.IsDefault ? 0 : 1;
+            Count = _first == default ? 0 : 1;
         }
 
         public BoxShadows(BoxShadow first, BoxShadow[] rest)
@@ -120,7 +120,7 @@ namespace Avalonia.Media
             get
             {
                 foreach(var boxShadow in this)
-                    if (!boxShadow.IsDefault && boxShadow.IsInset)
+                    if (boxShadow != default && boxShadow.IsInset)
                         return true;
                 return false;
             }

+ 1 - 1
src/Avalonia.Base/Media/DrawingGroup.cs

@@ -73,7 +73,7 @@ namespace Avalonia.Media
         {
             var bounds = GetBounds();
 
-            using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity))
+            using (context.PushTransform(Transform?.Value ?? Matrix.Identity))
             using (context.PushOpacity(Opacity, bounds))
             using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
             using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default)

+ 0 - 5
src/Avalonia.Base/Media/FontFamily.cs

@@ -79,11 +79,6 @@ namespace Avalonia.Media
         /// <remarks>Key is only used for custom fonts.</remarks>
         public FontFamilyKey? Key { get; }
 
-        /// <summary>
-        /// Returns <c>True</c> if this instance is the system's default.
-        /// </summary>
-        public bool IsDefault => Name.Equals(DefaultFontFamilyName);
-
         /// <summary>
         /// Implicit conversion of string to FontFamily
         /// </summary>

+ 3 - 2
src/Avalonia.Base/Media/FormattedText.cs

@@ -1393,10 +1393,11 @@ namespace Avalonia.Media
                 }
             }
 
-            if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsDefault)
+            if (accumulatedBounds?.PlatformImpl == null ||
+                (accumulatedBounds.PlatformImpl.Bounds.Width == 0 && accumulatedBounds.PlatformImpl.Bounds.Height == 0))
             {
                 return null;
-            }            
+            }
 
             return accumulatedBounds;
         }

+ 1 - 1
src/Avalonia.Base/Media/ImageDrawing.cs

@@ -42,7 +42,7 @@ namespace Avalonia.Media
             var imageSource = ImageSource;
             var rect = Rect;
 
-            if (imageSource is object && !rect.IsDefault)
+            if (imageSource is object && (rect.Width != 0 || rect.Height != 0))
             {
                 context.DrawImage(imageSource, rect);
             }

+ 1 - 1
src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs

@@ -77,7 +77,7 @@ namespace Avalonia.Media.Imaging
             {
                 if (Source is not IBitmap bmp)
                     return default;
-                if (SourceRect.IsDefault)
+                if (SourceRect.Width == 0 && SourceRect.Height == 0)
                     return Source.Size;
                 return SourceRect.Size.ToSizeWithDpi(bmp.Dpi);
             }

+ 2 - 17
src/Avalonia.Base/PixelRect.cs

@@ -9,12 +9,6 @@ namespace Avalonia
     /// </summary>
     public readonly struct PixelRect : IEquatable<PixelRect>
     {
-        /// <summary>
-        /// An empty rectangle.
-        /// </summary>
-        [Obsolete("Use the default keyword instead.")]
-        public static readonly PixelRect Empty = default;
-
         /// <summary>
         /// Initializes a new instance of the <see cref="PixelRect"/> structure.
         /// </summary>
@@ -133,15 +127,6 @@ namespace Avalonia
         /// </summary>
         public PixelPoint Center => new PixelPoint(X + (Width / 2), Y + (Height / 2));
 
-        /// <summary>
-        /// Gets a value indicating whether the instance has default values (the rectangle is empty).
-        /// </summary>
-        public bool IsDefault => Width == 0 && Height == 0;
-
-        /// <inheritdoc cref="IsDefault"/>
-        [Obsolete("Use IsDefault instead.")]
-        public bool IsEmpty => IsDefault;
-
         /// <summary>
         /// Checks for equality between two <see cref="PixelRect"/>s.
         /// </summary>
@@ -295,11 +280,11 @@ namespace Avalonia
         /// <returns>The union.</returns>
         public PixelRect Union(PixelRect rect)
         {
-            if (IsDefault)
+            if (Width == 0 && Height == 0)
             {
                 return rect;
             }
-            else if (rect.IsDefault)
+            else if (rect.Width == 0 && rect.Height == 0)
             {
                 return this;
             }

+ 16 - 1
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
 using System.Text;
@@ -23,7 +24,7 @@ internal static class StorageProviderHelpers
 
         return null;
     }
-    
+
     public static Uri FilePathToUri(string path)
     {
         var uriPath = new StringBuilder(path)
@@ -35,6 +36,20 @@ internal static class StorageProviderHelpers
         return new UriBuilder("file", string.Empty) { Path = uriPath }.Uri;
     }
     
+    public static bool TryFilePathToUri(string path, [NotNullWhen(true)] out Uri? uri)
+    {
+        try
+        {
+            uri = FilePathToUri(path);
+            return true;
+        }
+        catch
+        {
+            uri = null;
+            return false;
+        }
+    }
+    
     public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
     {
         var name = Path.GetFileName(path);

+ 13 - 3
src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs

@@ -16,8 +16,13 @@ public static class StorageProviderExtensions
         {
             return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile);
         }
-        
-        return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath));
+
+        if (StorageProviderHelpers.TryFilePathToUri(filePath, out var uri))
+        {
+            return provider.TryGetFileFromPathAsync(uri);
+        }
+
+        return Task.FromResult<IStorageFile?>(null);
     }
 
     /// <inheritdoc cref="IStorageProvider.TryGetFolderFromPathAsync"/>
@@ -29,7 +34,12 @@ public static class StorageProviderExtensions
             return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder);
         }
 
-        return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath));
+        if (StorageProviderHelpers.TryFilePathToUri(folderPath, out var uri))
+        {
+            return provider.TryGetFolderFromPathAsync(uri);
+        }
+
+        return Task.FromResult<IStorageFolder?>(null);
     }
 
     /// <summary>

+ 0 - 8
src/Avalonia.Base/Point.cs

@@ -288,13 +288,5 @@ namespace Avalonia
             x = this._x;
             y = this._y;
         }
-
-        /// <summary>
-        /// Gets a value indicating whether the X and Y coordinates are zero.
-        /// </summary>
-        public bool IsDefault
-        {
-            get { return (_x == 0) && (_y == 0); }
-        }
     }
 }

+ 8 - 26
src/Avalonia.Base/Rect.cs

@@ -16,12 +16,6 @@ namespace Avalonia
             Animation.Animation.RegisterAnimator<RectAnimator>(prop => typeof(Rect).IsAssignableFrom(prop.PropertyType));
         }
 
-        /// <summary>
-        /// An empty rectangle.
-        /// </summary>
-        [Obsolete("Use the default keyword instead.")]
-        public static readonly Rect Empty = default;
-
         /// <summary>
         /// The X position.
         /// </summary>
@@ -170,17 +164,6 @@ namespace Avalonia
         /// </summary>
         public Point Center => new Point(_x + (_width / 2), _y + (_height / 2));
 
-        /// <summary>
-        /// Gets a value indicating whether the instance has default values (the rectangle is empty).
-        /// </summary>
-        // ReSharper disable CompareOfFloatsByEqualityOperator
-        public bool IsDefault => _width == 0 && _height == 0;
-        // ReSharper restore CompareOfFloatsByEqualityOperator
-
-        /// <inheritdoc cref="IsDefault"/>
-        [Obsolete("Use IsDefault instead.")]
-        public bool IsEmpty => IsDefault;
-
         /// <summary>
         /// Checks for equality between two <see cref="Rect"/>s.
         /// </summary>
@@ -517,19 +500,18 @@ namespace Avalonia
             return rect;
         }
 
-
-            /// <summary>
-            /// Gets the union of two rectangles.
-            /// </summary>
-            /// <param name="rect">The other rectangle.</param>
-            /// <returns>The union.</returns>
-            public Rect Union(Rect rect)
+        /// <summary>
+        /// Gets the union of two rectangles.
+        /// </summary>
+        /// <param name="rect">The other rectangle.</param>
+        /// <returns>The union.</returns>
+        public Rect Union(Rect rect)
         {
-            if (IsDefault)
+            if (Width == 0 && Height == 0)
             {
                 return rect;
             }
-            else if (rect.IsDefault)
+            else if (rect.Width == 0 && rect.Height == 0)
             {
                 return this;
             }

+ 6 - 6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@@ -130,8 +130,8 @@ namespace Avalonia.Rendering.Composition.Server
             }
 
             _renderTarget ??= _compositor.CreateRenderTarget(_surfaces());
-            
-            if(_dirtyRect.IsDefault && !_redrawRequested)
+
+            if ((_dirtyRect.Width == 0 && _dirtyRect.Height == 0) && !_redrawRequested)
                 return;
 
             Revision++;
@@ -163,7 +163,7 @@ namespace Avalonia.Rendering.Composition.Server
                     _dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height);
                 }
 
-                if (!_dirtyRect.IsDefault)
+                if (_dirtyRect.Width != 0 || _dirtyRect.Height != 0)
                 {
                     using (var context = _layer.CreateDrawingContext())
                     {
@@ -260,7 +260,7 @@ namespace Avalonia.Rendering.Composition.Server
         
         public void AddDirtyRect(Rect rect)
         {
-            if(rect.IsDefault)
+            if (rect.Width == 0 && rect.Height == 0)
                 return;
             var snapped = SnapToDevicePixels(rect, Scaling);
             DebugEvents?.RectInvalidated(rect);
@@ -275,7 +275,7 @@ namespace Avalonia.Rendering.Composition.Server
 
         public void Dispose()
         {
-            if(_disposed)
+            if (_disposed)
                 return;
             _disposed = true;
             using (_compositor.RenderInterface.EnsureCurrent())
@@ -302,7 +302,7 @@ namespace Avalonia.Rendering.Composition.Server
         {
             if (_attachedVisuals.Remove(visual) && IsEnabled)
                 visual.Deactivate();
-            if(visual.IsVisibleInFrame)
+            if (visual.IsVisibleInFrame)
                 AddDirtyRect(visual.TransformedOwnContentBounds);
         }
 

+ 25 - 30
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@@ -23,21 +23,20 @@ namespace Avalonia.Rendering.Composition.Server
         private bool _isBackface;
         private Rect? _transformedClipBounds;
         private Rect _combinedTransformedClipBounds;
-        
+
         protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
         {
-            
         }
 
         public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
         {
-            if(Visible == false || IsVisibleInFrame == false)
+            if (Visible == false || IsVisibleInFrame == false)
                 return;
-            if(Opacity == 0)
+            if (Opacity == 0)
                 return;
 
             currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds);
-            if(currentTransformedClip.IsDefault)
+            if (currentTransformedClip.Width == 0 && currentTransformedClip.Height == 0)
                 return;
 
             Root!.RenderedVisuals++;
@@ -61,7 +60,7 @@ namespace Avalonia.Rendering.Composition.Server
                 canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
             if (Clip != null) 
                 canvas.PushGeometryClip(Clip);
-            if(OpacityMaskBrush != null)
+            if (OpacityMaskBrush != null)
                 canvas.PushOpacityMask(OpacityMaskBrush, boundsRect);
 
             RenderCore(canvas, currentTransformedClip);
@@ -78,12 +77,12 @@ namespace Avalonia.Rendering.Composition.Server
                 canvas.PopClip();
             if (AdornedVisual != null && AdornerIsClipped)
                 canvas.PopClip();
-            if(Opacity != 1)
+            if (Opacity != 1)
                 canvas.PopOpacity();
         }
 
         protected virtual bool HandlesClipToBounds => false;
-        
+
         private ReadbackData _readback0, _readback1, _readback2;
 
         /// <summary>
@@ -98,17 +97,17 @@ namespace Avalonia.Rendering.Composition.Server
                 return ref _readback1;
             return ref _readback2;
         }
-        
+
         public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity;
         public Matrix4x4 GlobalTransformMatrix { get; private set; }
 
         public virtual void Update(ServerCompositionTarget root)
         {
-            if(Parent == null && Root == null)
+            if (Parent == null && Root == null)
                 return;
-            
+
             var wasVisible = IsVisibleInFrame;
-            
+
             // Calculate new parent-relative transform
             if (_combinedTransformDirty)
             {
@@ -122,7 +121,7 @@ namespace Avalonia.Rendering.Composition.Server
             var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix4x4.Identity;
 
             var newTransform = CombinedTransformMatrix * parentTransform;
-            
+
             // Check if visual was moved and recalculate face orientation
             var positionChanged = false;
             if (GlobalTransformMatrix != newTransform)
@@ -134,23 +133,23 @@ namespace Avalonia.Rendering.Composition.Server
 
             var oldTransformedContentBounds = TransformedOwnContentBounds;
             var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds;
-            
+
             if (_parent?.IsDirtyComposition == true)
             {
                 IsDirtyComposition = true;
                 _isDirtyForUpdate = true;
             }
-            
+
             var invalidateOldBounds = _isDirtyForUpdate;
             var invalidateNewBounds = _isDirtyForUpdate;
 
             GlobalTransformMatrix = newTransform;
-            
+
             var ownBounds = OwnContentBounds;
             if (ownBounds != _oldOwnContentBounds || positionChanged)
             {
                 _oldOwnContentBounds = ownBounds;
-                if (ownBounds.IsDefault)
+                if (ownBounds.Width == 0 && ownBounds.Height == 0)
                     TransformedOwnContentBounds = default;
                 else
                     TransformedOwnContentBounds =
@@ -171,16 +170,16 @@ namespace Avalonia.Rendering.Composition.Server
                 AdornedVisual?._combinedTransformedClipBounds
                 ?? Parent?._combinedTransformedClipBounds
                 ?? new Rect(Root!.Size);
-            
+
             if (_transformedClipBounds != null)
                 _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value);
-            
+
             EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1);
 
             IsHitTestVisibleInFrame = _parent?.IsHitTestVisibleInFrame != false
                                       && Visible
                                       && !_isBackface
-                                      && !_combinedTransformedClipBounds.IsDefault;
+                                      && (_combinedTransformedClipBounds.Width != 0 || _combinedTransformedClipBounds.Height != 0);
 
             IsVisibleInFrame = IsHitTestVisibleInFrame
                                && _parent?.IsVisibleInFrame != false
@@ -213,11 +212,11 @@ namespace Avalonia.Rendering.Composition.Server
 
         void AddDirtyRect(Rect rc)
         {
-            if(rc == default)
+            if (rc == default)
                 return;
             Root?.AddDirtyRect(rc);
         }
-        
+
         /// <summary>
         /// Data that can be read from the UI thread
         /// </summary>
@@ -228,7 +227,7 @@ namespace Avalonia.Rendering.Composition.Server
             public long TargetId;
             public bool Visible;
         }
-        
+
         partial void DeserializeChangesExtra(BatchStreamReader c)
         {
             ValuesInvalidated();
@@ -245,9 +244,8 @@ namespace Avalonia.Rendering.Composition.Server
 
         protected virtual void OnDetachedFromRoot(ServerCompositionTarget target)
         {
-            
         }
-        
+
         partial void OnRootChanged()
         {
             if (Root != null)
@@ -256,12 +254,11 @@ namespace Avalonia.Rendering.Composition.Server
                 OnAttachedToRoot(Root);
             }
         }
-        
+
         protected virtual void OnAttachedToRoot(ServerCompositionTarget target)
         {
-            
         }
-        
+
         protected override void ValuesInvalidated()
         {
             _isDirtyForUpdate = true;
@@ -274,6 +271,4 @@ namespace Avalonia.Rendering.Composition.Server
         public Rect TransformedOwnContentBounds { get; set; }
         public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y);
     }
-
-
 }

+ 1 - 1
src/Avalonia.Base/Rendering/DirtyRects.cs

@@ -30,7 +30,7 @@ namespace Avalonia.Rendering
         /// </remarks>
         public void Add(Rect rect)
         {
-            if (!rect.IsDefault)
+            if (rect.Width != 0 || rect.Height != 0)
             {
                 for (var i = 0; i < _rects.Count; ++i)
                 {

+ 2 - 2
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@@ -83,7 +83,7 @@ namespace Avalonia.Rendering
                     }
                 }
 
-                using (context.PushPostTransform(m))
+                using (context.PushTransform(m))
                 using (context.PushOpacity(opacity, bounds))
                 using (clipToBounds
 #pragma warning disable CS0618 // Type or member is obsolete
@@ -95,7 +95,7 @@ namespace Avalonia.Rendering
 
                 using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default)
                 using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default)
-                using (context.PushTransformContainer())
+                using (context.PushTransform(Matrix.Identity))
                 {
                     visual.Render(context);
                     

+ 0 - 11
src/Avalonia.Base/Size.cs

@@ -27,12 +27,6 @@ namespace Avalonia
         /// </summary>
         public static readonly Size Infinity = new Size(double.PositiveInfinity, double.PositiveInfinity);
 
-        /// <summary>
-        /// A size representing zero.
-        /// </summary>
-        [Obsolete("Use the default keyword instead.")]
-        public static readonly Size Empty = new Size(0, 0);
-
         /// <summary>
         /// The width.
         /// </summary>
@@ -306,10 +300,5 @@ namespace Avalonia
             width = this._width;
             height = this._height;
         }
-
-        /// <summary>
-        /// Gets a value indicating whether the Width and Height values are zero.
-        /// </summary>
-        public bool IsDefault => (_width == 0) && (_height == 0);
     }
 }

+ 0 - 10
src/Avalonia.Base/Thickness.cs

@@ -97,10 +97,6 @@ namespace Avalonia
         /// </summary>
         public double Bottom => _bottom;
 
-        /// <inheritdoc cref="IsDefault"/>
-        [Obsolete("Use IsDefault instead.")]
-        public bool IsEmpty => IsDefault;
-
         /// <summary>
         /// Gets a value indicating whether all sides are equal.
         /// </summary>
@@ -293,11 +289,5 @@ namespace Avalonia
             right = this._right;
             bottom = this._bottom;
         }
-
-        /// <summary>
-        /// Gets a value indicating whether the instance has default values
-        /// (the left, top, right and bottom values are zero).
-        /// </summary>
-        public bool IsDefault => (_left == 0) && (_top == 0) && (_right == 0) && (_bottom == 0);
     }
 }

+ 0 - 8
src/Avalonia.Base/Vector.cs

@@ -360,13 +360,5 @@ namespace Avalonia
             x = this._x;
             y = this._y;
         }
-
-        /// <summary>
-        /// Gets a value indicating whether the X and Y components are zero.
-        /// </summary>
-        public bool IsDefault
-        {
-            get { return (_x == 0) && (_y == 0); }
-        }
     }
 }

+ 4 - 4
src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs

@@ -177,14 +177,14 @@ namespace Avalonia.Controls
                 }
 
                 bool? uneditedValue = editingCheckBox.IsChecked;
-                if(editingEventArgs is PointerPressedEventArgs args)
+                if (editingEventArgs is PointerPressedEventArgs args)
                 {
                     void ProcessPointerArgs()
                     {
                         // Editing was triggered by a mouse click
                         Point position = args.GetPosition(editingCheckBox);
                         Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
-                        if(rect.Contains(position))
+                        if (rect.Contains(position))
                         {
                             EditValue();
                         }
@@ -192,14 +192,14 @@ namespace Avalonia.Controls
                     
                     void OnLayoutUpdated(object sender, EventArgs e)
                     {
-                        if(!editingCheckBox.Bounds.IsDefault)
+                        if (editingCheckBox.Bounds.Width != 0 || editingCheckBox.Bounds.Height != 0)
                         {
                             editingCheckBox.LayoutUpdated -= OnLayoutUpdated;
                             ProcessPointerArgs();
                         }
                     }
 
-                    if(editingCheckBox.Bounds.IsDefault)
+                    if (editingCheckBox.Bounds.Width == 0 && editingCheckBox.Bounds.Height == 0)
                     {
                         editingCheckBox.LayoutUpdated += OnLayoutUpdated;
                     }

+ 2 - 2
src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs

@@ -441,7 +441,7 @@ namespace Avalonia.Controls
 
             _pendingViewportShift = default;
             _unshiftableShift = default;
-            if (_visibleWindow.IsDefault)
+            if (_visibleWindow.Width == 0 && _visibleWindow.Height == 0)
             {
                 // We got cleared.
                 _layoutExtent = default;
@@ -527,7 +527,7 @@ namespace Avalonia.Controls
         private void TryInvalidateMeasure()
         {
             // Don't invalidate measure if we have an invalid window.
-            if (!_visibleWindow.IsDefault)
+            if (_visibleWindow.Width != 0 || _visibleWindow.Height != 0)
             {
                 // We invalidate measure instead of just invalidating arrange because
                 // we don't invalidate measure in UpdateViewport if the view is changing to

+ 1 - 1
src/Avalonia.Controls/BorderVisual.cs

@@ -50,7 +50,7 @@ class CompositionBorderVisual : CompositionDrawListVisual
             if (ClipToBounds)
             {
                 var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y)));
-                if (_cornerRadius.IsDefault)
+                if (_cornerRadius == default)
                     canvas.PushClip(clipRect);
                 else
                     canvas.PushClip(new RoundedRect(clipRect, _cornerRadius));

+ 20 - 0
src/Avalonia.Controls/ContainerClearingEventArgs.cs

@@ -0,0 +1,20 @@
+using System;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Provides data for the <see cref="ItemsControl.ContainerClearing"/> event.
+    /// </summary>
+    public class ContainerClearingEventArgs : EventArgs
+    {
+        public ContainerClearingEventArgs(Control container)
+        {
+            Container = container;
+        }
+
+        /// <summary>
+        /// Gets the prepared container.
+        /// </summary>
+        public Control Container { get; }
+    }
+}

+ 32 - 0
src/Avalonia.Controls/ContainerIndexChangedEventArgs.cs

@@ -0,0 +1,32 @@
+using System;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Provides data for the <see cref="ItemsControl.ContainerIndexChanged"/> event.
+    /// </summary>
+    public class ContainerIndexChangedEventArgs : EventArgs
+    {
+        public ContainerIndexChangedEventArgs(Control container, int oldIndex, int newIndex)
+        {
+            Container = container;
+            OldIndex = oldIndex;
+            NewIndex = newIndex;
+        }
+
+        /// <summary>
+        /// Get the container for which the index changed.
+        /// </summary>
+        public Control Container { get; }
+
+        /// <summary>
+        /// Gets the index of the container after the change.
+        /// </summary>
+        public int NewIndex { get; }
+
+        /// <summary>
+        /// Gets the index of the container before the change.
+        /// </summary>
+        public int OldIndex { get; }
+    }
+}

+ 26 - 0
src/Avalonia.Controls/ContainerPreparedEventArgs.cs

@@ -0,0 +1,26 @@
+using System;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Provides data for the <see cref="ItemsControl.ContainerPrepared"/> event.
+    /// </summary>
+    public class ContainerPreparedEventArgs : EventArgs
+    {
+        public ContainerPreparedEventArgs(Control container, int index)
+        {
+            Container = container;
+            Index = index;
+        }
+
+        /// <summary>
+        /// Gets the prepared container.
+        /// </summary>
+        public Control Container { get; }
+
+        /// <summary>
+        /// Gets the index of the item the container was prepared for.
+        /// </summary>
+        public int Index { get; }
+    }
+}

+ 45 - 58
src/Avalonia.Controls/ContextMenu.cs

@@ -54,11 +54,17 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
             Popup.PlacementGravityProperty.AddOwner<ContextMenu>();
 
+        /// <summary>
+        /// Defines the <see cref="Placement"/> property.
+        /// </summary>
+        public static readonly StyledProperty<PlacementMode> PlacementProperty =
+            Popup.PlacementProperty.AddOwner<ContextMenu>();
+
         /// <summary>
         /// Defines the <see cref="PlacementMode"/> property.
         /// </summary>
-        public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
-            Popup.PlacementModeProperty.AddOwner<ContextMenu>();
+        [Obsolete("Use the Placement property instead.")]
+        public static readonly StyledProperty<PlacementMode> PlacementModeProperty = PlacementProperty;
 
         /// <summary>
         /// Defines the <see cref="PlacementRect"/> property.
@@ -108,99 +114,80 @@ namespace Avalonia.Controls
         static ContextMenu()
         {
             ItemsPanelProperty.OverrideDefaultValue<ContextMenu>(DefaultPanel);
-            PlacementModeProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
+            PlacementProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
             ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
             AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<ContextMenu>(AccessibilityView.Control);
             AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<ContextMenu>(AutomationControlType.Menu);
         }
 
-        /// <summary>
-        /// Gets or sets the Horizontal offset of the context menu in relation to the <see cref="PlacementTarget"/>.
-        /// </summary>
+        /// <inheritdoc cref="Popup.HorizontalOffset"/>
         public double HorizontalOffset
         {
-            get { return GetValue(HorizontalOffsetProperty); }
-            set { SetValue(HorizontalOffsetProperty, value); }
+            get => GetValue(HorizontalOffsetProperty);
+            set => SetValue(HorizontalOffsetProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets the Vertical offset of the context menu in relation to the <see cref="PlacementTarget"/>.
-        /// </summary>
+        /// <inheritdoc cref="Popup.VerticalOffset"/>
         public double VerticalOffset
         {
-            get { return GetValue(VerticalOffsetProperty); }
-            set { SetValue(VerticalOffsetProperty, value); }
+            get => GetValue(VerticalOffsetProperty);
+            set => SetValue(VerticalOffsetProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/>
-        /// is <see cref="PlacementMode.AnchorAndGravity"/>.
-        /// </summary>
+        /// <inheritdoc cref="Popup.PlacementAnchor"/>
         public PopupAnchor PlacementAnchor
         {
-            get { return GetValue(PlacementAnchorProperty); }
-            set { SetValue(PlacementAnchorProperty, value); }
+            get => GetValue(PlacementAnchorProperty);
+            set => SetValue(PlacementAnchorProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets a value describing how the context menu position will be adjusted if the
-        /// unadjusted position would result in the context menu being partly constrained.
-        /// </summary>
+        /// <inheritdoc cref="Popup.PlacementConstraintAdjustment"/>
         public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
         {
-            get { return GetValue(PlacementConstraintAdjustmentProperty); }
-            set { SetValue(PlacementConstraintAdjustmentProperty, value); }
+            get => GetValue(PlacementConstraintAdjustmentProperty);
+            set => SetValue(PlacementConstraintAdjustmentProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets a value which defines in what direction the context menu should open
-        /// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
-        /// </summary>
+        /// <inheritdoc cref="Popup.PlacementGravity"/>
         public PopupGravity PlacementGravity
         {
-            get { return GetValue(PlacementGravityProperty); }
-            set { SetValue(PlacementGravityProperty, value); }
+            get => GetValue(PlacementGravityProperty);
+            set => SetValue(PlacementGravityProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets the placement mode of the context menu in relation to the<see cref="PlacementTarget"/>.
-        /// </summary>
+        /// <inheritdoc cref="Placement"/>
+        [Obsolete("Use the Placement property instead.")]
         public PlacementMode PlacementMode
         {
-            get { return GetValue(PlacementModeProperty); }
-            set { SetValue(PlacementModeProperty, value); }
+            get => GetValue(PlacementProperty);
+            set => SetValue(PlacementProperty, value);
+        }
+
+        /// <inheritdoc cref="Popup.Placement"/>
+        public PlacementMode Placement
+        {
+            get => GetValue(PlacementProperty);
+            set => SetValue(PlacementProperty, value);
         }
 
         public bool WindowManagerAddShadowHint
         {
-            get { return GetValue(WindowManagerAddShadowHintProperty); }
-            set { SetValue(WindowManagerAddShadowHintProperty, value); }
+            get => GetValue(WindowManagerAddShadowHintProperty);
+            set => SetValue(WindowManagerAddShadowHintProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets the the anchor rectangle within the parent that the context menu will be placed
-        /// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
-        /// </summary>
-        /// <remarks>
-        /// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
-        /// which the popup will be opened, with <see cref="PlacementAnchor"/> determining which edge
-        /// of the placement target is used.
-        /// 
-        /// If unset, the anchor rectangle will be the bounds of the <see cref="PlacementTarget"/>.
-        /// </remarks>
+        /// <inheritdoc cref="Popup.PlacementRect"/>
         public Rect? PlacementRect
         {
-            get { return GetValue(PlacementRectProperty); }
-            set { SetValue(PlacementRectProperty, value); }
+            get => GetValue(PlacementRectProperty);
+            set => SetValue(PlacementRectProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets the control that is used to determine the popup's position.
-        /// </summary>
+        /// <inheritdoc cref="Popup.PlacementTarget"/>
         public Control? PlacementTarget
         {
-            get { return GetValue(PlacementTargetProperty); }
-            set { SetValue(PlacementTargetProperty, value); }
+            get => GetValue(PlacementTargetProperty);
+            set => SetValue(PlacementTargetProperty, value);
         }
 
         /// <summary>
@@ -343,9 +330,9 @@ namespace Avalonia.Controls
                 ((ISetLogicalParent)_popup).SetParent(control);
             }
 
-            _popup.PlacementMode = !requestedByPointer && PlacementMode == PlacementMode.Pointer
+            _popup.Placement = !requestedByPointer && Placement == PlacementMode.Pointer
                 ? PlacementMode.Bottom
-                : PlacementMode;
+                : Placement;
 
             //Position of the line below is really important. 
             //All styles are being applied only when control has logical parent.

+ 1 - 1
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@@ -389,7 +389,7 @@ namespace Avalonia.Controls
 
             _presenter.Date = SelectedDate ?? DateTimeOffset.Now;
 
-            _popup.PlacementMode = PlacementMode.AnchorAndGravity;
+            _popup.Placement = PlacementMode.AnchorAndGravity;
             _popup.PlacementAnchor = Primitives.PopupPositioning.PopupAnchor.Bottom;
             _popup.PlacementGravity = Primitives.PopupPositioning.PopupGravity.Bottom;
             _popup.PlacementConstraintAdjustment = Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;

+ 1 - 1
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@@ -255,7 +255,7 @@ namespace Avalonia.Controls
 
             _presenter.Time = SelectedTime ?? DateTime.Now.TimeOfDay;
 
-            _popup.PlacementMode = PlacementMode.AnchorAndGravity;
+            _popup.Placement = PlacementMode.AnchorAndGravity;
             _popup.PlacementAnchor = Primitives.PopupPositioning.PopupAnchor.Bottom;
             _popup.PlacementGravity = Primitives.PopupPositioning.PopupGravity.Bottom;
             _popup.PlacementConstraintAdjustment = Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;

+ 7 - 9
src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs

@@ -14,9 +14,9 @@ namespace Avalonia.Controls.Primitives
 {
     public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
     {
-        /// <inheritdoc cref="Popup.PlacementModeProperty"/>
+        /// <inheritdoc cref="Popup.PlacementProperty"/>
         public static readonly StyledProperty<PlacementMode> PlacementProperty =
-            Popup.PlacementModeProperty.AddOwner<PopupFlyoutBase>();
+            Popup.PlacementProperty.AddOwner<PopupFlyoutBase>();
 
         /// <inheritdoc cref="Popup.HorizontalOffsetProperty"/>
         public static readonly StyledProperty<double> HorizontalOffsetProperty =
@@ -64,15 +64,13 @@ namespace Avalonia.Controls.Primitives
 
         protected Popup Popup => _popupLazy.Value;
 
-        /// <summary>
-        /// Gets or sets the desired placement.
-        /// </summary>
+        /// <inheritdoc cref="Popup.Placement"/>
         public PlacementMode Placement
         {
             get => GetValue(PlacementProperty);
             set => SetValue(PlacementProperty, value);
         }
-        
+
         /// <inheritdoc cref="Popup.PlacementGravity"/>
         public PopupGravity PlacementGravity
         {
@@ -407,7 +405,7 @@ namespace Avalonia.Controls.Primitives
         {
             Size sz;
             // Popup.Child can't be null here, it was set in ShowAtCore.
-            if (Popup.Child!.DesiredSize.IsDefault)
+            if (Popup.Child!.DesiredSize == default)
             {
                 // Popup may not have been shown yet. Measure content
                 sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
@@ -423,11 +421,11 @@ namespace Avalonia.Controls.Primitives
             Popup.PlacementGravity = PlacementGravity;
             if (showAtPointer)
             {
-                Popup.PlacementMode = PlacementMode.Pointer;
+                Popup.Placement = PlacementMode.Pointer;
             }
             else
             {
-                Popup.PlacementMode = Placement;
+                Popup.Placement = Placement;
                 Popup.PlacementConstraintAdjustment =
                     PopupPositioning.PopupPositionerConstraintAdjustment.SlideX |
                     PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;

+ 31 - 0
src/Avalonia.Controls/ItemsControl.cs

@@ -274,6 +274,34 @@ namespace Avalonia.Controls
             remove => _childIndexChanged -= value;
         }
 
+        /// <summary>
+        /// Occurs each time a container is prepared for use.
+        /// </summary>
+        /// <remarks>
+        /// The prepared element might be newly created or an existing container that is being re-
+        /// used.
+        /// </remarks>
+        public event EventHandler<ContainerPreparedEventArgs>? ContainerPrepared;
+
+        /// <summary>
+        /// Occurs for each realized container when the index for the item it represents has changed.
+        /// </summary>
+        /// <remarks>
+        /// This event is raised for each realized container where the index for the item it
+        /// represents has changed. For example, when another item is added or removed in the data
+        /// source, the index for items that come after in the ordering will be impacted.
+        /// </remarks>
+        public event EventHandler<ContainerIndexChangedEventArgs>? ContainerIndexChanged;
+
+        /// <summary>
+        /// Occurs each time a container is cleared.
+        /// </summary>
+        /// <remarks>
+        /// This event is raised immediately each time an container is cleared, such as when it
+        /// falls outside the range of realized items or the corresponding item is removed.
+        /// </remarks>
+        public event EventHandler<ContainerClearingEventArgs>? ContainerClearing;
+
         /// <inheritdoc />
         public event EventHandler<RoutedEventArgs> HorizontalSnapPointsChanged
         {
@@ -649,18 +677,21 @@ namespace Avalonia.Controls
         {
             _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index));
             _scrollViewer?.RegisterAnchorCandidate(container);
+            ContainerPrepared?.Invoke(this, new(container, index));
         }
 
         internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex)
         {
             ContainerIndexChangedOverride(container, oldIndex, newIndex);
             _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex));
+            ContainerIndexChanged?.Invoke(this, new(container, oldIndex, newIndex));
         }
 
         internal void ClearItemContainer(Control container)
         {
             _scrollViewer?.UnregisterAnchorCandidate(container);
             ClearContainerForItemOverride(container);
+            ContainerClearing?.Invoke(this, new(container));
         }
 
         private void AddControlItemsToLogicalChildren(IEnumerable? items)

+ 2 - 2
src/Avalonia.Controls/LayoutTransformControl.cs

@@ -91,7 +91,7 @@ namespace Avalonia.Controls
             arrangedsize = TransformRoot.Bounds.Size;
 
             // This is the first opportunity under Silverlight to find out the Child's true DesiredSize
-            if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && _childActualSize.IsDefault)
+            if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && _childActualSize == default)
             {
                 //// Unfortunately, all the work so far is invalid because the wrong DesiredSize was used
                 //// Make a note of the actual DesiredSize
@@ -122,7 +122,7 @@ namespace Avalonia.Controls
             }
 
             Size measureSize;
-            if (_childActualSize.IsDefault)
+            if (_childActualSize == default)
             {
                 // Determine the largest size after the transformation
                 measureSize = ComputeLargestTransformedSize(availableSize);

+ 1 - 1
src/Avalonia.Controls/NativeControlHost.cs

@@ -141,7 +141,7 @@ namespace Avalonia.Controls
 
             if (IsEffectivelyVisible && bounds.HasValue)
             {
-                if (bounds.Value.IsDefault)
+                if (bounds.Value.Width == 0 && bounds.Value.Height == 0)
                     return false;
                 _attachment?.ShowInBounds(bounds.Value);
             }

+ 1 - 2
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@@ -69,8 +69,7 @@ namespace Avalonia.Controls.Presenters
                     var c = children[index + i];
                     if (!c.IsSet(ItemIsOwnContainerProperty))
                         itemsControl.RemoveLogicalChild(children[i + index]);
-                    else
-                        generator.ClearItemContainer(c);
+                    generator.ClearItemContainer(c);
                 }
 
                 children.RemoveRange(index, count);

+ 47 - 33
src/Avalonia.Controls/Primitives/Popup.cs

@@ -65,11 +65,17 @@ namespace Avalonia.Controls.Primitives
         public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
             AvaloniaProperty.Register<Popup, PopupGravity>(nameof(PlacementGravity));
 
+        /// <summary>
+        /// Defines the <see cref="Placement"/> property.
+        /// </summary>
+        public static readonly StyledProperty<PlacementMode> PlacementProperty =
+            AvaloniaProperty.Register<Popup, PlacementMode>(nameof(Placement), defaultValue: PlacementMode.Bottom);
+
         /// <summary>
         /// Defines the <see cref="PlacementMode"/> property.
         /// </summary>
-        public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
-            AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
+        [Obsolete("Use the Placement property instead.")]
+        public static readonly StyledProperty<PlacementMode> PlacementModeProperty = PlacementProperty;
 
         /// <summary>
         /// Defines the <see cref="PlacementRect"/> property.
@@ -146,8 +152,8 @@ namespace Avalonia.Controls.Primitives
 
         public bool WindowManagerAddShadowHint
         {
-            get { return GetValue(WindowManagerAddShadowHintProperty); }
-            set { SetValue(WindowManagerAddShadowHintProperty, value); }
+            get => GetValue(WindowManagerAddShadowHintProperty);
+            set => SetValue(WindowManagerAddShadowHintProperty, value);
         }
 
         /// <summary>
@@ -156,8 +162,8 @@ namespace Avalonia.Controls.Primitives
         [Content]
         public Control? Child
         {
-            get { return GetValue(ChildProperty); }
-            set { SetValue(ChildProperty, value); }
+            get => GetValue(ChildProperty);
+            set => SetValue(ChildProperty, value);
         }
 
         /// <summary>
@@ -205,13 +211,13 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <summary>
-        /// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/>
+        /// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="Placement"/>
         /// is <see cref="PlacementMode.AnchorAndGravity"/>.
         /// </summary>
         public PopupAnchor PlacementAnchor
         {
-            get { return GetValue(PlacementAnchorProperty); }
-            set { SetValue(PlacementAnchorProperty, value); }
+            get => GetValue(PlacementAnchorProperty);
+            set => SetValue(PlacementAnchorProperty, value);
         }
 
         /// <summary>
@@ -220,32 +226,40 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
         {
-            get { return GetValue(PlacementConstraintAdjustmentProperty); }
-            set { SetValue(PlacementConstraintAdjustmentProperty, value); }
+            get => GetValue(PlacementConstraintAdjustmentProperty);
+            set => SetValue(PlacementConstraintAdjustmentProperty, value);
         }
 
         /// <summary>
         /// Gets or sets a value which defines in what direction the popup should open
-        /// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
+        /// when <see cref="Placement"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
         /// </summary>
         public PopupGravity PlacementGravity
         {
-            get { return GetValue(PlacementGravityProperty); }
-            set { SetValue(PlacementGravityProperty, value); }
+            get => GetValue(PlacementGravityProperty);
+            set => SetValue(PlacementGravityProperty, value);
+        }
+
+        /// <inheritdoc cref="Placement"/>
+        [Obsolete("Use the Placement property instead.")]
+        public PlacementMode PlacementMode
+        {
+            get => GetValue(PlacementProperty);
+            set => SetValue(PlacementProperty, value);
         }
 
         /// <summary>
-        /// Gets or sets the placement mode of the popup in relation to the <see cref="PlacementTarget"/>.
+        /// Gets or sets the desired placement of the popup in relation to the <see cref="PlacementTarget"/>.
         /// </summary>
-        public PlacementMode PlacementMode
+        public PlacementMode Placement
         {
-            get { return GetValue(PlacementModeProperty); }
-            set { SetValue(PlacementModeProperty, value); }
+            get => GetValue(PlacementProperty);
+            set => SetValue(PlacementProperty, value);
         }
 
         /// <summary>
         /// Gets or sets the the anchor rectangle within the parent that the popup will be placed
-        /// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
+        /// relative to when <see cref="Placement"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
         /// </summary>
         /// <remarks>
         /// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
@@ -256,8 +270,8 @@ namespace Avalonia.Controls.Primitives
         /// </remarks>
         public Rect? PlacementRect
         {
-            get { return GetValue(PlacementRectProperty); }
-            set { SetValue(PlacementRectProperty, value); }
+            get => GetValue(PlacementRectProperty);
+            set => SetValue(PlacementRectProperty, value);
         }
 
         /// <summary>
@@ -266,8 +280,8 @@ namespace Avalonia.Controls.Primitives
         [ResolveByName]
         public Control? PlacementTarget
         {
-            get { return GetValue(PlacementTargetProperty); }
-            set { SetValue(PlacementTargetProperty, value); }
+            get => GetValue(PlacementTargetProperty);
+            set => SetValue(PlacementTargetProperty, value);
         }
 
         /// <summary>
@@ -301,8 +315,8 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public double HorizontalOffset
         {
-            get { return GetValue(HorizontalOffsetProperty); }
-            set { SetValue(HorizontalOffsetProperty, value); }
+            get => GetValue(HorizontalOffsetProperty);
+            set => SetValue(HorizontalOffsetProperty, value);
         }
 
         /// <summary>
@@ -310,8 +324,8 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public double VerticalOffset
         {
-            get { return GetValue(VerticalOffsetProperty); }
-            set { SetValue(VerticalOffsetProperty, value); }
+            get => GetValue(VerticalOffsetProperty);
+            set => SetValue(VerticalOffsetProperty, value);
         }
 
         /// <summary>
@@ -319,8 +333,8 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public bool Topmost
         {
-            get { return GetValue(TopmostProperty); }
-            set { SetValue(TopmostProperty, value); }
+            get => GetValue(TopmostProperty);
+            set => SetValue(TopmostProperty, value);
         }
 
         IPopupHost? IPopupHostProvider.PopupHost => Host;
@@ -404,7 +418,7 @@ namespace Avalonia.Controls.Primitives
                     (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup);
 
                 // Recalculate popup position on parent moved/resized, but not if placement was on pointer
-                if (PlacementMode != PlacementMode.Pointer)
+                if (Placement != PlacementMode.Pointer)
                 {
                     SubscribeToEventHandler<IWindowImpl, Action<PixelPoint>>(window.PlatformImpl, WindowPositionChanged,
                         (x, handler) => x.PositionChanged += handler,
@@ -534,7 +548,7 @@ namespace Avalonia.Controls.Primitives
                     UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget);
                 }
                 else if (change.Property == PlacementTargetProperty ||
-                         change.Property == PlacementModeProperty ||
+                         change.Property == PlacementProperty ||
                          change.Property == HorizontalOffsetProperty ||
                          change.Property == VerticalOffsetProperty ||
                          change.Property == PlacementAnchorProperty ||
@@ -567,7 +581,7 @@ namespace Avalonia.Controls.Primitives
         {
             popupHost.ConfigurePosition(
                 placementTarget,
-                PlacementMode,
+                Placement,
                 new Point(HorizontalOffset, VerticalOffset),
                 PlacementAnchor,
                 PlacementGravity,
@@ -615,7 +629,7 @@ namespace Avalonia.Controls.Primitives
                     return;
                 _openState.PopupHost.ConfigurePosition(
                     placementTarget,
-                    PlacementMode,
+                    Placement,
                     new Point(HorizontalOffset, VerticalOffset),
                     PlacementAnchor,
                     PlacementGravity,

+ 2 - 1
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@@ -112,7 +112,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
                                    ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry))
                                    ?? screens.FirstOrDefault();
 
-                if (targetScreen != null && targetScreen.WorkingArea.IsDefault)
+                if (targetScreen != null &&
+                    (targetScreen.WorkingArea.Width == 0 && targetScreen.WorkingArea.Height == 0))
                 {
                     return targetScreen.Bounds;
                 }

+ 1 - 1
src/Avalonia.Controls/SelectableTextBlock.cs

@@ -177,7 +177,7 @@ namespace Avalonia.Controls
 
                 var rects = TextLayout.HitTestTextRange(start, length);
 
-                using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
+                using (context.PushTransform(Matrix.CreateTranslation(origin)))
                 {
                     foreach (var rect in rects)
                     {

+ 1 - 1
src/Avalonia.Controls/SplitButton/SplitButton.cs

@@ -172,7 +172,7 @@ namespace Avalonia.Controls
                 flyout.Opened += Flyout_Opened;
                 flyout.Closed += Flyout_Closed;
 
-                _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(Popup.PlacementModeProperty).Subscribe(Flyout_PlacementPropertyChanged);
+                _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(Popup.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged);
             }
         }
 

+ 0 - 1
src/Avalonia.Controls/TopLevel.cs

@@ -15,7 +15,6 @@ using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Platform.Storage;
-using Avalonia.Reactive;
 using Avalonia.Rendering;
 using Avalonia.Styling;
 using Avalonia.Utilities;

+ 3 - 2
src/Avalonia.Controls/VirtualizingStackPanel.cs

@@ -459,7 +459,8 @@ namespace Avalonia.Controls
 
             while (c is not null)
             {
-                if (!c.Bounds.IsDefault && c.TransformToVisual(this) is Matrix transform)
+                if ((c.Bounds.Width != 0 || c.Bounds.Height != 0) &&
+                    c.TransformToVisual(this) is Matrix transform)
                 {
                     viewport = new Rect(0, 0, c.Bounds.Width, c.Bounds.Height)
                         .TransformToAABB(transform);
@@ -1078,7 +1079,7 @@ namespace Avalonia.Controls
                     // elements after the insertion point.
                     var elementCount = _elements.Count;
                     var start = Math.Max(realizedIndex, 0);
-                    var newIndex = first + count;
+                    var newIndex = realizedIndex + count;
 
                     for (var i = start; i < elementCount; ++i)
                     {

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml

@@ -127,7 +127,7 @@
 
             <Popup Name="PART_Popup" WindowManagerAddShadowHint="False"
                    IsLightDismissEnabled="True" PlacementTarget="{TemplateBinding}"
-                   PlacementMode="Bottom">
+                   Placement="Bottom">
               <DatePickerPresenter Name="PART_PickerPresenter" />
             </Popup>
 

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/Menu.xaml

@@ -37,7 +37,7 @@
                    MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
                    IsLightDismissEnabled="True"
                    IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
-                   PlacementMode="BottomEdgeAlignedLeft"
+                   Placement="BottomEdgeAlignedLeft"
                    OverlayInputPassThroughElement="{Binding $parent[Menu]}">
               <Border Background="{DynamicResource MenuFlyoutPresenterBackground}"
                       BorderBrush="{DynamicResource MenuFlyoutPresenterBorderBrush}"

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

@@ -118,7 +118,7 @@
           </Border>
           <Popup Name="PART_Popup"
                  WindowManagerAddShadowHint="False"
-                 PlacementMode="Right"
+                 Placement="Right"
                  HorizontalOffset="{DynamicResource MenuFlyoutSubItemPopupHorizontalOffset}"
                  IsLightDismissEnabled="False"
                  IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml

@@ -151,7 +151,7 @@
                    WindowManagerAddShadowHint="False"
                    IsLightDismissEnabled="True"
                    PlacementTarget="{TemplateBinding}"
-                   PlacementMode="Bottom">
+                   Placement="Bottom">
               <TimePickerPresenter Name="PART_PickerPresenter" />
             </Popup>
 

+ 1 - 1
src/Avalonia.Themes.Simple/Controls/DatePicker.xaml

@@ -142,7 +142,7 @@
 
             <Popup Name="PART_Popup"
                    IsLightDismissEnabled="True"
-                   PlacementMode="Bottom"
+                   Placement="Bottom"
                    PlacementTarget="{TemplateBinding}"
                    WindowManagerAddShadowHint="False">
               <DatePickerPresenter Name="PART_PickerPresenter" />

+ 1 - 1
src/Avalonia.Themes.Simple/Controls/Menu.xaml

@@ -27,7 +27,7 @@
             <Popup Name="PART_Popup"
                    IsLightDismissEnabled="True"
                    IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
-                   PlacementMode="BottomEdgeAlignedLeft"
+                   Placement="BottomEdgeAlignedLeft"
                    OverlayInputPassThroughElement="{Binding $parent[Menu]}">
               <Border Background="{DynamicResource ThemeBackgroundBrush}"
                       BorderBrush="{DynamicResource ThemeBorderMidBrush}"

+ 1 - 1
src/Avalonia.Themes.Simple/Controls/MenuItem.xaml

@@ -65,7 +65,7 @@
                    IsLightDismissEnabled="False"
                    IsOpen="{TemplateBinding IsSubMenuOpen,
                                             Mode=TwoWay}"
-                   PlacementMode="Right">
+                   Placement="Right">
               <Border Background="{DynamicResource ThemeBackgroundBrush}"
                       BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderThickness="{TemplateBinding BorderThickness}">

+ 1 - 1
src/Avalonia.Themes.Simple/Controls/TimePicker.xaml

@@ -156,7 +156,7 @@
 
             <Popup Name="PART_Popup"
                    IsLightDismissEnabled="True"
-                   PlacementMode="Bottom"
+                   Placement="Bottom"
                    PlacementTarget="{TemplateBinding}"
                    WindowManagerAddShadowHint="False">
               <TimePickerPresenter Name="PART_PickerPresenter" />

+ 2 - 2
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -375,7 +375,7 @@ namespace Avalonia.Skia
 
             foreach (var boxShadow in boxShadows)
             {
-                if (!boxShadow.IsDefault && !boxShadow.IsInset)
+                if (boxShadow != default && !boxShadow.IsInset)
                 {
                     using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity))
                     {
@@ -432,7 +432,7 @@ namespace Avalonia.Skia
 
             foreach (var boxShadow in boxShadows)
             {
-                if (!boxShadow.IsDefault && boxShadow.IsInset)
+                if (boxShadow != default && boxShadow.IsInset)
                 {
                     using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity))
                     {

+ 1 - 1
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@@ -303,7 +303,7 @@ namespace Avalonia.Controls.UnitTests
                 window.Show();
 
                 var c = new ContextMenu();
-                c.PlacementMode = PlacementMode.Bottom;
+                c.Placement = PlacementMode.Bottom;
                 c.Open(button);
 
                 var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);

+ 130 - 0
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
 using System.Linq;
@@ -831,6 +832,135 @@ namespace Avalonia.Controls.UnitTests
             Assert.Throws<InvalidOperationException>(() => target.DisplayMemberBinding = new Binding("Length"));
         }
 
+        [Fact]
+        public void ContainerPrepared_Is_Raised_For_Each_Item_Container_On_Layout()
+        {
+            var target = new ItemsControl
+            {
+                Template = GetTemplate(),
+                Items = { "Foo", "Bar", "Baz" },
+            };
+
+            var result = new List<Control>();
+            var index = 0;
+
+            target.ContainerPrepared += (s, e) =>
+            {
+                Assert.Equal(index++, e.Index);
+                result.Add(e.Container);
+            };
+
+            target.ApplyTemplate();
+            target.Presenter.ApplyTemplate();
+
+            Assert.Equal(3, result.Count);
+            Assert.Equal(target.GetRealizedContainers(), result);
+        }
+
+        [Fact]
+        public void ContainerPrepared_Is_Raised_For_Each_ItemsSource_Container_On_Layout()
+        {
+            var target = new ItemsControl
+            {
+                Template = GetTemplate(),
+                ItemsSource = new[] { "Foo", "Bar", "Baz" },
+            };
+
+            var result = new List<Control>();
+            var index = 0;
+
+            target.ContainerPrepared += (s, e) =>
+            {
+                Assert.Equal(index++, e.Index);
+                result.Add(e.Container);
+            };
+
+            target.ApplyTemplate();
+            target.Presenter.ApplyTemplate();
+
+            Assert.Equal(3, result.Count);
+            Assert.Equal(target.GetRealizedContainers(), result);
+        }
+
+        [Fact]
+        public void ContainerPrepared_Is_Raised_For_Added_Item()
+        {
+            var target = new ItemsControl
+            {
+                Template = GetTemplate(),
+                Items = { "Foo", "Bar", "Baz" },
+            };
+
+            target.ApplyTemplate();
+            target.Presenter.ApplyTemplate();
+
+            var result = new List<Control>();
+
+            target.ContainerPrepared += (s, e) =>
+            {
+                Assert.Equal(3, e.Index);
+                result.Add(e.Container);
+            };
+
+            target.Items.Add("Qux");
+
+            Assert.Equal(1, result.Count);
+        }
+
+        [Fact]
+        public void ContainerIndexChanged_Is_Raised_When_Item_Added()
+        {
+            var target = new ItemsControl
+            {
+                Template = GetTemplate(),
+                Items = { "Foo", "Bar", "Baz" },
+            };
+
+            target.ApplyTemplate();
+            target.Presenter.ApplyTemplate();
+
+            var result = new List<Control>();
+            var index = 1;
+
+            target.ContainerIndexChanged += (s, e) =>
+            {
+                Assert.Equal(index++, e.OldIndex);
+                Assert.Equal(index, e.NewIndex);
+                result.Add(e.Container);
+            };
+
+            target.Items.Insert(1, "Qux");
+
+            Assert.Equal(2, result.Count);
+            Assert.Equal(target.GetRealizedContainers().Skip(2), result);
+        }
+
+        [Fact]
+        public void ContainerClearing_Is_Raised_When_Item_Removed()
+        {
+            var target = new ItemsControl
+            {
+                Template = GetTemplate(),
+                Items = { "Foo", "Bar", "Baz" },
+            };
+
+            target.ApplyTemplate();
+            target.Presenter.ApplyTemplate();
+
+            var expected = target.ContainerFromIndex(1);
+            var raised = 0;
+
+            target.ContainerClearing += (s, e) =>
+            {
+                Assert.Same(expected, e.Container);
+                ++raised;
+            };
+
+            target.Items.RemoveAt(1);
+
+            Assert.Equal(1, raised);
+        }
+
         private class Item
         {
             public Item(string value)

+ 140 - 0
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@@ -785,6 +785,146 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void ContainerPrepared_Is_Raised_For_Each_Item_Container_On_Layout()
+        {
+            using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+            var target = new ListBox
+            {
+                Template = ListBoxTemplate(),
+                Items = { "Foo", "Bar", "Baz" },
+            };
+
+            var result = new List<Control>();
+            var index = 0;
+
+            target.ContainerPrepared += (s, e) =>
+            {
+                Assert.Equal(index++, e.Index);
+                result.Add(e.Container);
+            };
+
+            Prepare(target);
+
+            Assert.Equal(3, result.Count);
+            Assert.Equal(target.GetRealizedContainers(), result);
+        }
+
+        [Fact]
+        public void ContainerPrepared_Is_Raised_For_Each_ItemsSource_Container_On_Layout()
+        {
+            using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+            var target = new ListBox
+            {
+                Template = ListBoxTemplate(),
+                ItemsSource = new[] { "Foo", "Bar", "Baz" },
+            };
+
+            var result = new List<Control>();
+            var index = 0;
+
+            target.ContainerPrepared += (s, e) =>
+            {
+                Assert.Equal(index++, e.Index);
+                result.Add(e.Container);
+            };
+
+            Prepare(target);
+
+            Assert.Equal(3, result.Count);
+            Assert.Equal(target.GetRealizedContainers(), result);
+        }
+
+        [Fact]
+        public void ContainerPrepared_Is_Raised_For_Added_Item()
+        {
+            using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+            var data = new AvaloniaList<string> { "Foo", "Bar", "Baz" };
+            var target = new ListBox
+            {
+                Template = ListBoxTemplate(),
+                ItemsSource = data,
+            };
+
+            Prepare(target);
+
+            var result = new List<Control>();
+
+            target.ContainerPrepared += (s, e) =>
+            {
+                Assert.Equal(3, e.Index);
+                result.Add(e.Container);
+            };
+
+            data.Add("Qux");
+            Layout(target);
+
+            Assert.Equal(1, result.Count);
+        }
+
+        [Fact]
+        public void ContainerIndexChanged_Is_Raised_When_Item_Added()
+        {
+            using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+            var data = new AvaloniaList<string> { "Foo", "Bar", "Baz" };
+            var target = new ListBox
+            {
+                Template = ListBoxTemplate(),
+                ItemsSource = data,
+            };
+
+            Prepare(target);
+
+            var result = new List<Control>();
+            var index = 1;
+
+            target.ContainerIndexChanged += (s, e) =>
+            {
+                Assert.Equal(index++, e.OldIndex);
+                Assert.Equal(index, e.NewIndex);
+                result.Add(e.Container);
+            };
+
+            data.Insert(1, "Qux");
+            Layout(target);
+
+            Assert.Equal(2, result.Count);
+            Assert.Equal(target.GetRealizedContainers().Skip(2), result);
+        }
+
+        [Fact]
+        public void ContainerClearing_Is_Raised_When_Item_Removed()
+        {
+            using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+            var data = new AvaloniaList<string> { "Foo", "Bar", "Baz" };
+            var target = new ListBox
+            {
+                Template = ListBoxTemplate(),
+                ItemsSource = data,
+            };
+
+            Prepare(target);
+
+            var expected = target.ContainerFromIndex(1);
+            var raised = 0;
+
+            target.ContainerClearing += (s, e) =>
+            {
+                Assert.Same(expected, e.Container);
+                ++raised;
+            };
+
+            data.RemoveAt(1);
+            Layout(target);
+
+            Assert.Equal(1, raised);
+        }
+
         private class ResettingCollection : List<string>, INotifyCollectionChanged
         {
             public ResettingCollection(int itemCount)

+ 1 - 1
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@@ -67,7 +67,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
                 var window = new Window();
-                var target = new Popup {PlacementMode = PlacementMode.Pointer};
+                var target = new Popup {Placement = PlacementMode.Pointer};
                 var child = new Control();
 
                 window.Content = target;

+ 10 - 10
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@@ -207,7 +207,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             using (CreateServices())
             {
-                var target = new Popup() {PlacementMode = PlacementMode.Pointer};
+                var target = new Popup() {Placement = PlacementMode.Pointer};
                 var root = PreparedWindow(target);
 
                 target.Open();
@@ -226,7 +226,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             using (CreateServices())
             {
                 var window = PreparedWindow();
-                var target = new Popup() {PlacementMode = PlacementMode.Pointer};
+                var target = new Popup() {Placement = PlacementMode.Pointer};
 
                 window.Content = target;
 
@@ -249,7 +249,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             using (CreateServices())
             {
                 var window = PreparedWindow();
-                var target = new Popup() {PlacementMode = PlacementMode.Pointer};
+                var target = new Popup() {Placement = PlacementMode.Pointer};
 
                 window.Content = target;
                 window.ApplyTemplate();
@@ -274,7 +274,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             using (CreateServices())
             {
                 var window = PreparedWindow();
-                var target = new Popup() { PlacementMode = PlacementMode.Pointer };
+                var target = new Popup() { Placement = PlacementMode.Pointer };
 
                 window.Content = target;
                 window.ApplyTemplate();
@@ -742,7 +742,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 {
                     Width = 400,
                     Height = 200,
-                    PlacementMode = PlacementMode.Pointer
+                    Placement = PlacementMode.Pointer
                 };
                 var window = PreparedWindow(popup);
                 window.Show();
@@ -791,7 +791,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 var popup = new Popup()
                 {
                     PlacementTarget = placementTarget,
-                    PlacementMode = PlacementMode.Bottom,
+                    Placement = PlacementMode.Bottom,
                     Width = 10,
                     Height = 10
                 };
@@ -852,7 +852,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 var popup = new Popup()
                 {
                     PlacementTarget = placementTarget,
-                    PlacementMode = PlacementMode.Pointer,
+                    Placement = PlacementMode.Pointer,
                     Width = 10,
                     Height = 10
                 };
@@ -907,7 +907,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 var popup = new Popup()
                 {
                     PlacementTarget = placementTarget,
-                    PlacementMode = PlacementMode.Bottom,
+                    Placement = PlacementMode.Bottom,
                     Width = 10,
                     Height = 10
                 };
@@ -967,7 +967,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 var popup = new Popup()
                 {
                     PlacementTarget = placementTarget,
-                    PlacementMode = PlacementMode.Pointer,
+                    Placement = PlacementMode.Pointer,
                     Width = 10,
                     Height = 10
                 };
@@ -1020,7 +1020,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                     Width = 20,
                     Height = 20,
                     PlacementTarget = parentPopup, 
-                    PlacementMode = PlacementMode.AnchorAndGravity,
+                    Placement = PlacementMode.AnchorAndGravity,
                     PlacementAnchor = PopupAnchor.TopLeft,
                     PlacementGravity = PopupGravity.BottomRight
                 };

+ 30 - 0
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@@ -406,6 +406,36 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void ContainerPrepared_Is_Raised_When_Scrolling()
+        {
+            using var app = App();
+            var (target, scroll, itemsControl) = CreateTarget();
+            var raised = 0;
+
+            itemsControl.ContainerPrepared += (s, e) => ++raised;
+
+            scroll.Offset = new Vector(0, 200);
+            Layout(target);
+
+            Assert.Equal(10, raised);
+        }
+
+        [Fact]
+        public void ContainerClearing_Is_Raised_When_Scrolling()
+        {
+            using var app = App();
+            var (target, scroll, itemsControl) = CreateTarget();
+            var raised = 0;
+
+            itemsControl.ContainerClearing += (s, e) => ++raised;
+
+            scroll.Offset = new Vector(0, 200);
+            Layout(target);
+
+            Assert.Equal(10, raised);
+        }
+
         private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
         {
             return target.GetRealizedElements()

+ 13 - 6
tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs

@@ -11,19 +11,26 @@ using Xunit;
 
 namespace Avalonia.IntegrationTests.Appium
 {
+    public record class WindowChrome(
+        AppiumWebElement? Close,
+        AppiumWebElement? Minimize,
+        AppiumWebElement? Maximize,
+        AppiumWebElement? FullScreen);
+
     internal static class ElementExtensions
     {
         public static IReadOnlyList<AppiumWebElement> GetChildren(this AppiumWebElement element) =>
             element.FindElementsByXPath("*/*");
 
-        public static (AppiumWebElement close, AppiumWebElement minimize, AppiumWebElement maximize) GetChromeButtons(this AppiumWebElement window)
+        public static WindowChrome GetChromeButtons(this AppiumWebElement window)
         {
             if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
             {
-                var closeButton = window.FindElementByXPath("//XCUIElementTypeButton[1]");
-                var fullscreenButton = window.FindElementByXPath("//XCUIElementTypeButton[2]");
-                var minimizeButton = window.FindElementByXPath("//XCUIElementTypeButton[3]");
-                return (closeButton, minimizeButton, fullscreenButton);
+                var closeButton = window.FindElementsByAccessibilityId("_XCUI:CloseWindow").FirstOrDefault();
+                var fullscreenButton = window.FindElementsByAccessibilityId("_XCUI:FullScreenWindow").FirstOrDefault();
+                var minimizeButton = window.FindElementsByAccessibilityId("_XCUI:MinimizeWindow").FirstOrDefault();
+                var zoomButton = window.FindElementsByAccessibilityId("_XCUI:ZoomWindow").FirstOrDefault();
+                return new(closeButton, minimizeButton, zoomButton, fullscreenButton);
             }
 
             throw new NotSupportedException("GetChromeButtons not supported on this platform.");
@@ -138,7 +145,7 @@ namespace Avalonia.IntegrationTests.Appium
                     var text = windows.Select(x => x.Text).ToList();
                     var newWindow = session.FindElements(By.XPath("/XCUIElementTypeApplication/XCUIElementTypeWindow"))
                         .First(x => x.Text == newWindowTitle);
-                    var (close, _, _) = ((AppiumWebElement)newWindow).GetChromeButtons();
+                    var close = ((AppiumWebElement)newWindow).FindElementByAccessibilityId("_XCUI:CloseWindow");
                     close!.Click();
                     Thread.Sleep(1000);
                 });

+ 11 - 2
tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs

@@ -17,6 +17,7 @@ namespace Avalonia
     internal class PlatformFactAttribute : FactAttribute
     {
         private readonly string? _reason;
+        private string? _skip;
 
         public PlatformFactAttribute(TestPlatforms platforms, string? reason = null)
         {
@@ -28,8 +29,16 @@ namespace Avalonia
 
         public override string? Skip
         {
-            get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}" + (_reason is not null ? $" reason: \"{_reason}\"" : "");
-            set => throw new NotSupportedException();
+            get
+            {
+                if (_skip is not null)
+                    return _skip;
+                if (!IsSupported())
+                    return $"Ignored on {RuntimeInformation.OSDescription}" +
+                           (_reason is not null ? $" reason: '{_reason}'" : "");
+                return null;
+            }
+            set => _skip = value;
         }
 
         private bool IsSupported()

+ 9 - 2
tests/Avalonia.IntegrationTests.Appium/PlatformTheoryAttribute.cs

@@ -7,14 +7,21 @@ namespace Avalonia.IntegrationTests.Appium
 {
     internal class PlatformTheoryAttribute : TheoryAttribute
     {
+        private string? _skip;
+
         public PlatformTheoryAttribute(TestPlatforms platforms = TestPlatforms.All) => Platforms = platforms;
 
         public TestPlatforms Platforms { get; }
 
         public override string? Skip
         {
-            get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}";
-            set => throw new NotSupportedException();
+            get
+            {
+                if (_skip is not null)
+                    return _skip;
+                return !IsSupported() ? $"Ignored on {RuntimeInformation.OSDescription}" : null;
+            }
+            set => _skip = value;
         }
 
         private bool IsSupported()

+ 102 - 22
tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs

@@ -83,9 +83,9 @@ namespace Avalonia.IntegrationTests.Appium
         public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_In_Fullscreen()
         {
             var mainWindow = GetWindow("MainWindow");
-            var buttons = mainWindow.GetChromeButtons();
+            var fullScreen = mainWindow.FindElementByAccessibilityId("_XCUI:FullScreenWindow");
 
-            buttons.maximize.Click();
+            fullScreen.Click();
 
             Thread.Sleep(500);
 
@@ -239,17 +239,18 @@ namespace Avalonia.IntegrationTests.Appium
         public void Parent_Window_Has_Disabled_ChromeButtons_When_Modal_Dialog_Shown()
         {
             var window = GetWindow("MainWindow");
-            var (closeButton, miniaturizeButton, zoomButton) = window.GetChromeButtons();
+            var windowChrome = window.GetChromeButtons();
 
-            Assert.True(closeButton.Enabled);
-            Assert.True(zoomButton.Enabled);
-            Assert.True(miniaturizeButton.Enabled);
+            Assert.True(windowChrome.Close!.Enabled);
+            Assert.True(windowChrome.FullScreen!.Enabled);
+            Assert.True(windowChrome.Minimize!.Enabled);
+            Assert.Null(windowChrome.Maximize);
 
             using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner))
             {
-                Assert.False(closeButton.Enabled);
-                Assert.False(zoomButton.Enabled);
-                Assert.False(miniaturizeButton.Enabled);
+                Assert.False(windowChrome.Close!.Enabled);
+                Assert.False(windowChrome.FullScreen!.Enabled);
+                Assert.False(windowChrome.Minimize!.Enabled);
             }
         }
 
@@ -259,11 +260,11 @@ namespace Avalonia.IntegrationTests.Appium
             using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner))
             {
                 var secondaryWindow = GetWindow("SecondaryWindow");
-                var (closeButton, miniaturizeButton, zoomButton) = secondaryWindow.GetChromeButtons();
+                var windowChrome = secondaryWindow.GetChromeButtons();
 
-                Assert.True(closeButton.Enabled);
-                Assert.True(zoomButton.Enabled);
-                Assert.False(miniaturizeButton.Enabled);
+                Assert.True(windowChrome.Close!.Enabled);
+                Assert.True(windowChrome.Maximize!.Enabled);
+                Assert.False(windowChrome.Minimize!.Enabled);
             }
         }
         
@@ -274,7 +275,7 @@ namespace Avalonia.IntegrationTests.Appium
             using (OpenWindow(new PixelSize(200, 100), mode, WindowStartupLocation.Manual))
             {
                 var secondaryWindow = GetWindow("SecondaryWindow");
-                var (_, miniaturizeButton, _) = secondaryWindow.GetChromeButtons();
+                var miniaturizeButton = secondaryWindow.FindElementByAccessibilityId("_XCUI:MinimizeWindow");
 
                 Assert.False(miniaturizeButton.Enabled);
             }
@@ -288,7 +289,7 @@ namespace Avalonia.IntegrationTests.Appium
             using (OpenWindow(new PixelSize(200, 100), mode, WindowStartupLocation.Manual))
             {
                 var secondaryWindow = GetWindow("SecondaryWindow");
-                var (_, miniaturizeButton, _) = secondaryWindow.GetChromeButtons();
+                var miniaturizeButton = secondaryWindow.FindElementByAccessibilityId("_XCUI:MinimizeWindow");
 
                 miniaturizeButton.Click();
                 Thread.Sleep(1000);
@@ -332,7 +333,7 @@ namespace Avalonia.IntegrationTests.Appium
 
             // Close the window manually.
             secondaryWindow = GetWindow("SecondaryWindow");
-            secondaryWindow.GetChromeButtons().close.Click();
+            secondaryWindow.FindElementByAccessibilityId("_XCUI:CloseWindow").Click();
         }
 
         [PlatformTheory(TestPlatforms.MacOS)]
@@ -344,22 +345,92 @@ namespace Avalonia.IntegrationTests.Appium
             using (OpenWindow(null, mode, WindowStartupLocation.Manual, canResize: false))
             {
                 var secondaryWindow = GetWindow("SecondaryWindow");
-                var (_, _, zoomButton) = secondaryWindow.GetChromeButtons();
+                var zoomButton = mode == ShowWindowMode.NonOwned ?
+                    secondaryWindow.FindElementByAccessibilityId("_XCUI:FullScreenWindow") :
+                    secondaryWindow.FindElementByAccessibilityId("_XCUI:ZoomWindow");
                 Assert.False(zoomButton.Enabled);
             }
         }
-        
+
+        [PlatformFact(TestPlatforms.MacOS)]
+        public void Toggling_SystemDecorations_Should_Preserve_ExtendClientArea()
+        {
+            // #10650
+            using (OpenWindow(extendClientArea: true))
+            {
+                var secondaryWindow = GetWindow("SecondaryWindow");
+                
+                // The XPath of the title bar text _should_ be "XCUIElementTypeStaticText"
+                // but Appium seems to put a fake node between the window and the title bar
+                // https://stackoverflow.com/a/71914227/6448
+                var titleBar = secondaryWindow.FindElementsByXPath("/*/XCUIElementTypeStaticText").Count;
+                
+                Assert.Equal(0, titleBar);
+
+                secondaryWindow.FindElementByAccessibilityId("CurrentSystemDecorations").Click();
+                _session.FindElementByAccessibilityId("SystemDecorationsNone").SendClick();
+                secondaryWindow.FindElementByAccessibilityId("CurrentSystemDecorations").Click();
+                _session.FindElementByAccessibilityId("SystemDecorationsFull").SendClick();
+
+                titleBar = secondaryWindow.FindElementsByXPath("/*/XCUIElementTypeStaticText").Count;
+                Assert.Equal(0, titleBar);
+            }
+        }
+
+        [PlatformTheory(TestPlatforms.MacOS)]
+        [InlineData(SystemDecorations.None)]
+        [InlineData(SystemDecorations.BorderOnly)]
+        [InlineData(SystemDecorations.Full)]
+        public void ExtendClientArea_SystemDecorations_Shows_Correct_Buttons(SystemDecorations decorations)
+        {
+            // #10650
+            using (OpenWindow(extendClientArea: true, systemDecorations: decorations))
+            {
+                var secondaryWindow = GetWindow("SecondaryWindow");
+
+                try
+                {
+                    var chrome = secondaryWindow.GetChromeButtons();
+                
+                    if (decorations == SystemDecorations.Full)
+                    {
+                        Assert.NotNull(chrome.Close);
+                        Assert.NotNull(chrome.Minimize);
+                        Assert.NotNull(chrome.FullScreen);
+                    }
+                    else
+                    {
+                        Assert.Null(chrome.Close);
+                        Assert.Null(chrome.Minimize);
+                        Assert.Null(chrome.FullScreen);
+                    }
+                }
+                finally
+                {
+                    if (decorations != SystemDecorations.Full)
+                    {
+                        secondaryWindow.FindElementByAccessibilityId("CurrentSystemDecorations").Click();
+                        _session.FindElementByAccessibilityId("SystemDecorationsFull").SendClick();
+                    }
+                }
+            }
+        }
+
         private IDisposable OpenWindow(
-            PixelSize? size,
-            ShowWindowMode mode,
-            WindowStartupLocation location,
-            bool canResize = true)
+            PixelSize? size = null,
+            ShowWindowMode mode = ShowWindowMode.NonOwned,
+            WindowStartupLocation location = WindowStartupLocation.Manual,
+            bool canResize = true,
+            SystemDecorations systemDecorations = SystemDecorations.Full,
+            bool extendClientArea = false)
         {
             var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize");
             var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode");
             var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation");
             var canResizeCheckBox = _session.FindElementByAccessibilityId("ShowWindowCanResize");
             var showButton = _session.FindElementByAccessibilityId("ShowWindow");
+            var systemDecorationsComboBox = _session.FindElementByAccessibilityId("ShowWindowSystemDecorations");
+            var extendClientAreaCheckBox = _session.FindElementByAccessibilityId("ShowWindowExtendClientAreaToDecorationsHint");
 
             if (size.HasValue)
                 sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}");
@@ -379,6 +450,15 @@ namespace Avalonia.IntegrationTests.Appium
             if (canResizeCheckBox.GetIsChecked() != canResize)
                 canResizeCheckBox.Click();
 
+            if (systemDecorationsComboBox.GetComboBoxValue() != systemDecorations.ToString())
+            {
+                systemDecorationsComboBox.Click();
+                _session.FindElementByName(systemDecorations.ToString()).SendClick();
+            }
+            
+            if (extendClientAreaCheckBox.GetIsChecked() != extendClientArea)
+                extendClientAreaCheckBox.Click();
+            
             return showButton.OpenWindowWithClick();
         }
 

+ 2 - 2
tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs

@@ -88,7 +88,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Height = 200,
                 Child = new CustomRenderer((control, context) =>
                 {
-                    using (var transform = context.PushPreTransform(Matrix.CreateTranslation(100, 100)))
+                    using (var transform = context.PushTransform(Matrix.CreateTranslation(100, 100)))
                     using (var clip = context.PushClip(new Rect(0, 0, 100, 100)))
                     {
                         context.FillRectangle(Brushes.Blue, new Rect(0, 0, 200, 200));
@@ -112,7 +112,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
                 Height = 200,
                 Child = new CustomRenderer((control, context) =>
                 {
-                    using (var transform = context.PushPreTransform(Matrix.CreateTranslation(100, 100)))
+                    using (var transform = context.PushTransform(Matrix.CreateTranslation(100, 100)))
                     using (var clip = context.PushClip(new Rect(0, 0, 100, 100)))
                     {
                         context.FillRectangle(Brushes.Blue, new Rect(0, 0, 200, 200));

+ 1 - 1
tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs

@@ -200,7 +200,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                 Child = new DrawnControl(c =>
                 {
                     c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
-                    using (c.PushPreTransform(Matrix.CreateTranslation(100, 100)))
+                    using (c.PushTransform(Matrix.CreateTranslation(100, 100)))
                         c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
                 }),
             };

+ 1 - 1
tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs

@@ -95,7 +95,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                 {
                     c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
 
-                    using (c.PushPreTransform(Matrix.CreateTranslation(100, 100)))
+                    using (c.PushTransform(Matrix.CreateTranslation(100, 100)))
                         c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
                 }),
             };

+ 1 - 1
tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs

@@ -185,7 +185,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                 Child = new DrawnControl(c =>
                 {
                     c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
-                    using (c.PushPreTransform(Matrix.CreateTranslation(100, 100)))
+                    using (c.PushTransform(Matrix.CreateTranslation(100, 100)))
                         c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
                 }),
             };

+ 1 - 1
tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs

@@ -312,7 +312,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                     var rotate = Matrix.CreateTranslation(-100, -100) *
                         Matrix.CreateRotation(MathUtilities.Deg2Rad(90)) *
                         Matrix.CreateTranslation(100, 100);
-                    using var transform = c.PushPreTransform(rotate);
+                    using var transform = c.PushTransform(rotate);
                     c.DrawRectangle(Brushes.Yellow, null, rect);
                     t.Draw(c, rect.Position);
                 }),

+ 1 - 1
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@@ -792,7 +792,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             public override double Baseline => 0;
             public override void Draw(DrawingContext drawingContext, Point origin)
             {
-                using (drawingContext.PushPreTransform(Matrix.CreateTranslation(new Vector(origin.X, 0))))
+                using (drawingContext.PushTransform(Matrix.CreateTranslation(new Vector(origin.X, 0))))
                 {
                     drawingContext.FillRectangle(_fill, _rect);
                 }