Browse Source

Merge branch 'master' into refactor/value-store

Steven Kirk 5 years ago
parent
commit
9de65221e2
62 changed files with 1117 additions and 345 deletions
  1. 3 0
      native/Avalonia.Native/src/OSX/window.mm
  2. 1 1
      readme.md
  3. 5 1
      samples/ControlCatalog/MainView.xaml
  4. 46 39
      samples/ControlCatalog/Pages/ImagePage.xaml
  5. 20 19
      samples/ControlCatalog/Pages/ImagePage.xaml.cs
  6. 1 1
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  7. 0 64
      scripts/avalonia-rename.ps1
  8. 1 1
      src/Avalonia.Animation/IterationCount.cs
  9. 3 1
      src/Avalonia.Controls/DrawingPresenter.cs
  10. 27 18
      src/Avalonia.Controls/Image.cs
  11. 1 1
      src/Avalonia.Controls/Remote/RemoteWidget.cs
  12. 18 16
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  13. 61 16
      src/Avalonia.Controls/Repeater/ViewManager.cs
  14. 4 2
      src/Avalonia.Controls/TextBox.cs
  15. 7 7
      src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs
  16. 23 9
      src/Avalonia.Layout/FlowLayoutAlgorithm.cs
  17. 8 8
      src/Avalonia.Layout/NonVirtualizingLayout.cs
  18. 14 0
      src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
  19. 2 0
      src/Avalonia.Layout/StackLayout.cs
  20. 43 11
      src/Avalonia.Layout/UniformGridLayout.cs
  21. 26 8
      src/Avalonia.Layout/UniformGridLayoutState.cs
  22. 1 1
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  23. 6 8
      src/Avalonia.Styling/Controls/ISetResourceParent.cs
  24. 60 2
      src/Avalonia.Styling/Controls/ResourceDictionary.cs
  25. 2 2
      src/Avalonia.Styling/StyledElement.cs
  26. 6 6
      src/Avalonia.Styling/Styling/Style.cs
  27. 10 10
      src/Avalonia.Styling/Styling/Styles.cs
  28. 4 2
      src/Avalonia.Visuals/Media/Drawing.cs
  29. 16 5
      src/Avalonia.Visuals/Media/DrawingContext.cs
  30. 2 1
      src/Avalonia.Visuals/Media/DrawingGroup.cs
  31. 81 0
      src/Avalonia.Visuals/Media/DrawingImage.cs
  32. 4 1
      src/Avalonia.Visuals/Media/GeometryDrawing.cs
  33. 29 0
      src/Avalonia.Visuals/Media/IImage.cs
  34. 20 0
      src/Avalonia.Visuals/Media/Imaging/Bitmap.cs
  35. 1 10
      src/Avalonia.Visuals/Media/Imaging/IBitmap.cs
  36. 77 13
      src/Avalonia.Visuals/Media/MediaExtensions.cs
  37. 25 0
      src/Avalonia.Visuals/Media/StretchDirection.cs
  38. 2 2
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  39. 3 3
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  40. 3 1
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  41. 2 2
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  42. 1 1
      src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
  43. 1 1
      src/Avalonia.X11/X11IconLoader.cs
  44. 1 1
      src/Avalonia.X11/X11Platform.cs
  45. 25 1
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs
  46. 5 5
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  47. 1 1
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  48. 4 4
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  49. 4 1
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  50. 2 2
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  51. 1 1
      src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs
  52. 2 1
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs
  53. 1 1
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
  54. 6 8
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
  55. 62 8
      tests/Avalonia.Controls.UnitTests/ImageTests.cs
  56. 24 0
      tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
  57. 123 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs
  58. 1 1
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  59. 18 0
      tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
  60. 30 15
      tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs
  61. 1 1
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  62. 136 0
      tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs

+ 3 - 0
native/Avalonia.Native/src/OSX/window.mm

@@ -1283,6 +1283,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     _closed = false;
     
     _lastScaling = [self backingScaleFactor];
+    [self setOpaque:NO];
+    [self setBackgroundColor: [NSColor clearColor]];
+    [self invalidateShadow];
     return self;
 }
 

+ 1 - 1
readme.md

@@ -22,7 +22,7 @@ Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?it
 
 For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core).
 
-Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed))
+Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/)
 
 Use these commands in the Package Manager console to install Avalonia manually:
 ```

+ 5 - 1
samples/ControlCatalog/MainView.xaml

@@ -32,7 +32,11 @@
       <TabItem Header="DatePicker"><pages:DatePickerPage/></TabItem>
       <TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
       <TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
-      <TabItem Header="Image"><pages:ImagePage/></TabItem>
+      <TabItem Header="Image"
+               ScrollViewer.VerticalScrollBarVisibility="Disabled"
+               ScrollViewer.HorizontalScrollBarVisibility="Disabled">
+        <pages:ImagePage/>
+      </TabItem>
       <TabItem Header="ItemsRepeater"
                ScrollViewer.VerticalScrollBarVisibility="Disabled"
                ScrollViewer.HorizontalScrollBarVisibility="Disabled">

+ 46 - 39
samples/ControlCatalog/Pages/ImagePage.xaml

@@ -1,45 +1,52 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="ControlCatalog.Pages.ImagePage">
-  <StackPanel Orientation="Vertical" Spacing="4">
-    <TextBlock Classes="h1">Image</TextBlock>
-    <TextBlock Classes="h2">Displays an image</TextBlock>
-
-    <StackPanel Orientation="Horizontal"
-                Margin="0,16,0,0"
-                HorizontalAlignment="Center"
-                Spacing="16">
-      <StackPanel Orientation="Vertical">
-        <TextBlock>No Stretch</TextBlock>
-        <Image Source="/Assets/delicate-arch-896885_640.jpg"
-               Width="100" Height="200"
-               Stretch="None"/>
-      </StackPanel>
-
-      <StackPanel Orientation="Vertical">
-        <TextBlock>Fill</TextBlock>
-        <Image Source="/Assets/delicate-arch-896885_640.jpg"
-               Width="100" Height="200"
-               Stretch="Fill"/>
-      </StackPanel>
+  <DockPanel>
+    <StackPanel DockPanel.Dock="Top" Orientation="Vertical" Spacing="4">
+      <TextBlock Classes="h1">Image</TextBlock>
+      <TextBlock Classes="h2">Displays an image</TextBlock>
+    </StackPanel>
 
-      <StackPanel Orientation="Vertical">
-        <TextBlock>Uniform</TextBlock>
-        <Image Source="/Assets/delicate-arch-896885_640.jpg"
-                Width="100" Height="200"
-                Stretch="Uniform"/>
-      </StackPanel>
+    <Grid ColumnDefinitions="*,*" RowDefinitions="Auto,*" Margin="64">
+      
+      <DockPanel Grid.Column="0" Grid.Row="1" Margin="16">
+        <TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Bitmap</TextBlock>
+        <ComboBox Name="bitmapStretch" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="BitmapStretchChanged">
+          <ComboBoxItem>None</ComboBoxItem>
+          <ComboBoxItem>Fill</ComboBoxItem>
+          <ComboBoxItem>Uniform</ComboBoxItem>
+          <ComboBoxItem>UniformToFill</ComboBoxItem>
+        </ComboBox>
+        <Image Name="bitmapImage"
+               Source="/Assets/delicate-arch-896885_640.jpg"/>
+      </DockPanel>
 
-      <StackPanel Orientation="Vertical">
-        <TextBlock>UniformToFill</TextBlock>
-        <Image Source="/Assets/delicate-arch-896885_640.jpg"
-               Width="100" Height="200"
-               Stretch="UniformToFill"/>
-      </StackPanel>
-    </StackPanel>
-    <StackPanel Orientation="Vertical">
-      <TextBlock>Window Icon as an Image</TextBlock>
-      <Image Name="Icon" Width="100" Height="200" Stretch="None" />
-    </StackPanel>
-  </StackPanel>
+      <DockPanel Grid.Column="1" Grid.Row="1" Margin="16">
+        <TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Drawing</TextBlock>
+        <ComboBox Name="drawingStretch" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="DrawingStretchChanged">
+          <ComboBoxItem>None</ComboBoxItem>
+          <ComboBoxItem>Fill</ComboBoxItem>
+          <ComboBoxItem>Uniform</ComboBoxItem>
+          <ComboBoxItem>UniformToFill</ComboBoxItem>
+        </ComboBox>
+        <Image Name="drawingImage">
+          <Image.Source>
+            <DrawingImage>
+              <GeometryDrawing Brush="Red">
+                <PathGeometry>
+                  <PathFigure StartPoint="0,0" IsClosed="True">
+                    <QuadraticBezierSegment Point1="50,0" Point2="50,-50" />
+                    <QuadraticBezierSegment Point1="100,-50" Point2="100,0" />
+                    <LineSegment Point="50,0" />
+                    <LineSegment Point="50,50" />
+                  </PathFigure>
+                </PathGeometry>
+              </GeometryDrawing>
+            </DrawingImage>
+          </Image.Source>
+        </Image>
+      </DockPanel>
+    </Grid>
+    
+  </DockPanel>
 </UserControl>

+ 20 - 19
samples/ControlCatalog/Pages/ImagePage.xaml.cs

@@ -1,40 +1,41 @@
-using System.IO;
-using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
-using Avalonia.Media.Imaging;
+using Avalonia.Media;
 
 namespace ControlCatalog.Pages
 {
     public class ImagePage : UserControl
     {
-        private Image iconImage;
+        private readonly Image _bitmapImage;
+        private readonly Image _drawingImage;
+
         public ImagePage()
         {
-            this.InitializeComponent();
+            InitializeComponent();
+            _bitmapImage = this.FindControl<Image>("bitmapImage");
+            _drawingImage = this.FindControl<Image>("drawingImage");
         }
 
         private void InitializeComponent()
         {
             AvaloniaXamlLoader.Load(this);
-            iconImage = this.Get<Image>("Icon");
         }
 
-        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        public void BitmapStretchChanged(object sender, SelectionChangedEventArgs e)
+        {
+            if (_bitmapImage != null)
+            {
+                var comboxBox = (ComboBox)sender;
+                _bitmapImage.Stretch = (Stretch)comboxBox.SelectedIndex;
+            }
+        }
+
+        public void DrawingStretchChanged(object sender, SelectionChangedEventArgs e)
         {
-            base.OnAttachedToVisualTree(e);
-            if (iconImage.Source == null)
+            if (_drawingImage != null)
             {
-                var windowRoot = e.Root as Window;
-                if (windowRoot != null)
-                {
-                    using (var stream = new MemoryStream())
-                    {
-                        windowRoot.Icon.Save(stream);
-                        stream.Seek(0, SeekOrigin.Begin);
-                        iconImage.Source = new Bitmap(stream);
-                    }
-                }
+                var comboxBox = (ComboBox)sender;
+                _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex;
             }
         }
     }

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

@@ -39,7 +39,7 @@ namespace RenderDemo.Pages
                 ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100));
             }
 
-            context.DrawImage(_bitmap, 1, 
+            context.DrawImage(_bitmap,
                 new Rect(0, 0, 200, 200), 
                 new Rect(0, 0, 200, 200));
             Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);

+ 0 - 64
scripts/avalonia-rename.ps1

@@ -1,64 +0,0 @@
-function Get-NewDirectoryName {
-    param ([System.IO.DirectoryInfo]$item)
-
-    $name = $item.Name.Replace("perspex", "avalonia")
-    $name = $name.Replace("Perspex", "Avalonia")
-    Join-Path $item.Parent.FullName $name
-}
-
-function Get-NewFileName {
-    param ([System.IO.FileInfo]$item)
-
-    $name = $item.Name.Replace("perspex", "avalonia")
-    $name = $name.Replace("Perspex", "Avalonia")
-    Join-Path $item.DirectoryName $name
-}
-
-function Rename-Contents {
-    param ([System.IO.FileInfo] $file)
-
-    $extensions = @(".cs",".xaml",".csproj",".sln",".md",".json",".yml",".partial",".ps1",".nuspec",".htm",".html",".gitmodules".".xml",".plist",".targets",".projitems",".shproj",".xib")
-
-    if ($extensions.Contains($file.Extension)) {
-        $text = [IO.File]::ReadAllText($file.FullName)
-        $text = $text.Replace("github.com/perspex", "github.com/avaloniaui")
-        $text = $text.Replace("github.com/Perspex", "github.com/AvaloniaUI")
-        $text = $text.Replace("perspex", "avalonia")
-        $text = $text.Replace("Perspex", "Avalonia")
-        $text = $text.Replace("PERSPEX", "AVALONIA")
-        [IO.File]::WriteAllText($file.FullName, $text)
-    }
-}
-
-function Process-Files {
-    param ([System.IO.DirectoryInfo] $item)
-
-    $dirs = Get-ChildItem -Path $item.FullName -Directory
-    $files = Get-ChildItem -Path $item.FullName -File
-
-    foreach ($dir in $dirs) {
-        Process-Files $dir.FullName
-    }
-
-    foreach ($file in $files) {
-        Rename-Contents $file
-
-        $renamed = Get-NewFileName $file
-
-        if ($file.FullName -ne $renamed) {
-            Write-Host git mv $file.FullName $renamed
-            & git mv $file.FullName $renamed
-        }
-    }
-
-    $renamed = Get-NewDirectoryName $item
-
-    if ($item.FullName -ne $renamed) {
-        Write-Host git mv $item.FullName $renamed
-        & git mv $item.FullName $renamed
-    }
-}
-
-& git submodule deinit .
-& git clean -xdf
-Process-Files .

+ 1 - 1
src/Avalonia.Animation/IterationCount.cs

@@ -63,7 +63,7 @@ namespace Avalonia.Animation
         public IterationType RepeatType => _type;
 
         /// <summary>
-        /// Gets a value that indicates whether the <see cref="IterationCount"/> is set to loop.
+        /// Gets a value that indicates whether the <see cref="IterationCount"/> is set to Infinite.
         /// </summary>
         public bool IsInfinite => _type == IterationType.Infinite;
 

+ 3 - 1
src/Avalonia.Controls/DrawingPresenter.cs

@@ -1,9 +1,11 @@
-using Avalonia.Controls.Shapes;
+using System;
+using Avalonia.Controls.Shapes;
 using Avalonia.Media;
 using Avalonia.Metadata;
 
 namespace Avalonia.Controls
 {
+    [Obsolete("Use Image control with DrawingImage source")]
     public class DrawingPresenter : Control
     {
         static DrawingPresenter()

+ 27 - 18
src/Avalonia.Controls/Image.cs

@@ -14,8 +14,8 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="Source"/> property.
         /// </summary>
-        public static readonly StyledProperty<IBitmap> SourceProperty =
-            AvaloniaProperty.Register<Image, IBitmap>(nameof(Source));
+        public static readonly StyledProperty<IImage> SourceProperty =
+            AvaloniaProperty.Register<Image, IImage>(nameof(Source));
 
         /// <summary>
         /// Defines the <see cref="Stretch"/> property.
@@ -23,6 +23,14 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<Stretch> StretchProperty =
             AvaloniaProperty.Register<Image, Stretch>(nameof(Stretch), Stretch.Uniform);
 
+        /// <summary>
+        /// Defines the <see cref="StretchDirection"/> property.
+        /// </summary>
+        public static readonly StyledProperty<StretchDirection> StretchDirectionProperty =
+            AvaloniaProperty.Register<Image, StretchDirection>(
+                nameof(StretchDirection),
+                StretchDirection.Both);
+
         static Image()
         {
             AffectsRender<Image>(SourceProperty, StretchProperty);
@@ -30,9 +38,9 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Gets or sets the bitmap image that will be displayed.
+        /// Gets or sets the image that will be displayed.
         /// </summary>
-        public IBitmap Source
+        public IImage Source
         {
             get { return GetValue(SourceProperty); }
             set { SetValue(SourceProperty, value); }
@@ -43,10 +51,19 @@ namespace Avalonia.Controls
         /// </summary>
         public Stretch Stretch
         {
-            get { return (Stretch)GetValue(StretchProperty); }
+            get { return GetValue(StretchProperty); }
             set { SetValue(StretchProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets a value controlling in what direction the image will be stretched.
+        /// </summary>
+        public StretchDirection StretchDirection
+        {
+            get { return GetValue(StretchDirectionProperty); }
+            set { SetValue(StretchDirectionProperty, value); }
+        }
+
         /// <summary>
         /// Renders the control.
         /// </summary>
@@ -58,8 +75,8 @@ namespace Avalonia.Controls
             if (source != null)
             {
                 Rect viewPort = new Rect(Bounds.Size);
-                Size sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height);
-                Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize);
+                Size sourceSize = source.Size;
+                Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection);
                 Size scaledSize = sourceSize * scale;
                 Rect destRect = viewPort
                     .CenterRect(new Rect(scaledSize))
@@ -69,7 +86,7 @@ namespace Avalonia.Controls
 
                 var interpolationMode = RenderOptions.GetBitmapInterpolationMode(this);
 
-                context.DrawImage(source, 1, sourceRect, destRect, interpolationMode);
+                context.DrawImage(source, sourceRect, destRect, interpolationMode);
             }
         }
 
@@ -85,15 +102,7 @@ namespace Avalonia.Controls
 
             if (source != null)
             {
-                Size sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height);
-                if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height))
-                {
-                    result = sourceSize;
-                }
-                else
-                {
-                    result = Stretch.CalculateSize(availableSize, sourceSize);
-                }
+                result = Stretch.CalculateSize(availableSize, source.Size, StretchDirection);
             }
 
             return result;
@@ -106,7 +115,7 @@ namespace Avalonia.Controls
 
             if (source != null)
             {
-                var sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height);
+                var sourceSize = source.Size;
                 var result = Stretch.CalculateSize(finalSize, sourceSize);
                 return result;
             }

+ 1 - 1
src/Avalonia.Controls/Remote/RemoteWidget.cs

@@ -83,7 +83,7 @@ namespace Avalonia.Controls.Remote
                         Marshal.Copy(_lastFrame.Data, y * _lastFrame.Stride,
                             new IntPtr(l.Address.ToInt64() + l.RowBytes * y), lineLen);
                 }
-                context.DrawImage(_bitmap, 1, new Rect(0, 0, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height),
+                context.DrawImage(_bitmap, new Rect(0, 0, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height),
                     new Rect(Bounds.Size));
             }
             base.Render(context);

+ 18 - 16
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@@ -559,33 +559,35 @@ namespace Avalonia.Controls
 
             if (Layout != null)
             {
-                if (Layout is VirtualizingLayout virtualLayout)
-                {
-                    var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+                var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
 
+                try
+                {
                     _processingItemsSourceChange = args;
 
-                    try
+                    if (Layout is VirtualizingLayout virtualLayout)
                     {
                         virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
                     }
-                    finally
-                    {
-                        _processingItemsSourceChange = null;
-                    }
-                }
-                else if (Layout is NonVirtualizingLayout nonVirtualLayout)
-                {
-                    // Walk through all the elements and make sure they are cleared for
-                    // non-virtualizing layouts.
-                    foreach (var element in Children)
+                    else if (Layout is NonVirtualizingLayout nonVirtualLayout)
                     {
-                        if (GetVirtualizationInfo(element).IsRealized)
+                        // Walk through all the elements and make sure they are cleared for
+                        // non-virtualizing layouts.
+                        foreach (var element in Children)
                         {
-                            ClearElementImpl(element);
+                            if (GetVirtualizationInfo(element).IsRealized)
+                            {
+                                ClearElementImpl(element);
+                            }
                         }
+
+                        Children.Clear();
                     }
                 }
+                finally
+                {
+                    _processingItemsSourceChange = null;
+                }
 
                 InvalidateMeasure();
             }

+ 61 - 16
src/Avalonia.Controls/Repeater/ViewManager.cs

@@ -109,11 +109,22 @@ namespace Avalonia.Controls
 
         public void ClearElementToElementFactory(IControl element)
         {
-            var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
-            var clearedIndex = virtInfo.Index;
             _owner.OnElementClearing(element);
-            _owner.ItemTemplateShim.RecycleElement(_owner, element);
 
+            if (_owner.ItemTemplateShim != null)
+            {
+                _owner.ItemTemplateShim.RecycleElement(_owner, element);
+            }
+            else
+            {
+                // No ItemTemplate to recycle to, remove the element from the children collection.
+                if (!_owner.Children.Remove(element))
+                {
+                    throw new InvalidOperationException("ItemsRepeater's child not found in its Children collection.");
+                }
+            }
+
+            var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
             virtInfo.MoveOwnershipToElementFactory();
 
             if (_lastFocusedElement == element)
@@ -121,9 +132,8 @@ namespace Avalonia.Controls
                 // Focused element is going away. Remove the tracked last focused element
                 // and pick a reasonable next focus if we can find one within the layout 
                 // realized elements.
-                MoveFocusFromClearedIndex(clearedIndex);
+                MoveFocusFromClearedIndex(virtInfo.Index);
             }
-
         }
 
         private void MoveFocusFromClearedIndex(int clearedIndex)
@@ -190,7 +200,8 @@ namespace Avalonia.Controls
         {
             if (virtInfo == null)
             {
-                throw new ArgumentException("Element is not a child of this ItemsRepeater.");
+                //Element is not a child of this ItemsRepeater.
+                return -1;
             }
 
             return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1;
@@ -515,21 +526,52 @@ namespace Avalonia.Controls
             return element;
         }
 
+        // There are several cases handled here with respect to which element gets returned and when DataContext is modified.
+        //
+        // 1. If there is no ItemTemplate:
+        //    1.1 If data is an IControl -> the data is returned
+        //    1.2 If data is not an IControl -> a default DataTemplate is used to fetch element and DataContext is set to data
+        //
+        // 2. If there is an ItemTemplate:
+        //    2.1 If data is not an IControl -> Element is fetched from ElementFactory and DataContext is set to the data
+        //    2.2 If data is an IControl:
+        //        2.2.1 If Element returned by the ElementFactory is the same as the data -> Element (a.k.a. data) is returned as is
+        //        2.2.2 If Element returned by the ElementFactory is not the same as the data
+        //                 -> Element that is fetched from the ElementFactory is returned and
+        //                    DataContext is set to the data's DataContext (if it exists), otherwise it is set to the data itself
         private IControl GetElementFromElementFactory(int index)
         {
             // The view generator is the provider of last resort.
+            var data = _owner.ItemsSourceView.GetAt(index);
+            var providedElementFactory = _owner.ItemTemplateShim;
+
+            ItemTemplateWrapper GetElementFactory()
+            {
+                if (providedElementFactory == null)
+                {
+                    var factory = FuncDataTemplate.Default;
+                    _owner.ItemTemplate = factory;
+                    return _owner.ItemTemplateShim;
+                }
 
-            var itemTemplateFactory = _owner.ItemTemplateShim;
-            if (itemTemplateFactory == null)
+                return providedElementFactory;
+            }
+
+            IControl GetElement()
             {
-                // If no ItemTemplate was provided, use a default 
-                var factory = FuncDataTemplate.Default;
-                _owner.ItemTemplate = factory;
-                itemTemplateFactory = _owner.ItemTemplateShim;
+                if (providedElementFactory == null)
+                {
+                    if (data is IControl dataAsElement)
+                    {
+                        return dataAsElement;
+                    }
+                }
+
+                var elementFactory = GetElementFactory();
+                return elementFactory.GetElement(_owner, data);
             }
 
-            var data = _owner.ItemsSourceView.GetAt(index);
-            var element = itemTemplateFactory.GetElement(_owner, data);
+            var element = GetElement();
 
             var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
             if (virtInfo == null)
@@ -537,8 +579,11 @@ namespace Avalonia.Controls
                 virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
             }
 
-            // Prepare the element
-            element.DataContext = data;
+            if (data != element)
+            {
+                // Prepare the element
+                element.DataContext = data;
+            }
 
             virtInfo.MoveOwnershipToLayoutFromElementFactory(
                 index,

+ 4 - 2
src/Avalonia.Controls/TextBox.cs

@@ -390,8 +390,10 @@ namespace Avalonia.Controls
             {
                 return;
             }
+
             _undoRedoHelper.Snapshot();
             HandleTextInput(text);
+            _undoRedoHelper.Snapshot();
         }
 
         protected override void OnKeyDown(KeyEventArgs e)
@@ -401,12 +403,12 @@ namespace Avalonia.Controls
             bool movement = false;
             bool selection = false;
             bool handled = false;
-            var modifiers = e.Modifiers;
+            var modifiers = e.KeyModifiers;
 
             var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
 
             bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
-            bool DetectSelection() => e.Modifiers.HasFlag(keymap.SelectionModifiers);
+            bool DetectSelection() => e.KeyModifiers.HasFlag(keymap.SelectionModifiers);
 
             if (Match(keymap.SelectAll))
             {

+ 7 - 7
src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs

@@ -4,14 +4,14 @@ namespace Avalonia.Input.Platform
 {
     public class PlatformHotkeyConfiguration
     {
-        public PlatformHotkeyConfiguration() : this(InputModifiers.Control)
+        public PlatformHotkeyConfiguration() : this(KeyModifiers.Control)
         {
             
         }
         
-        public PlatformHotkeyConfiguration(InputModifiers commandModifiers,
-            InputModifiers selectionModifiers = InputModifiers.Shift,
-            InputModifiers wholeWordTextActionModifiers = InputModifiers.Control)
+        public PlatformHotkeyConfiguration(KeyModifiers commandModifiers,
+            KeyModifiers selectionModifiers = KeyModifiers.Shift,
+            KeyModifiers wholeWordTextActionModifiers = KeyModifiers.Control)
         {
             CommandModifiers = commandModifiers;
             SelectionModifiers = selectionModifiers;
@@ -75,9 +75,9 @@ namespace Avalonia.Input.Platform
             };
         }
         
-        public InputModifiers CommandModifiers { get; set; }
-        public InputModifiers WholeWordTextActionModifiers { get; set; }
-        public InputModifiers SelectionModifiers { get; set; }
+        public KeyModifiers CommandModifiers { get; set; }
+        public KeyModifiers WholeWordTextActionModifiers { get; set; }
+        public KeyModifiers SelectionModifiers { get; set; }
         public List<KeyGesture> Copy { get; set; }
         public List<KeyGesture> Cut { get; set; }
         public List<KeyGesture> Paste { get; set; }

+ 23 - 9
src/Avalonia.Layout/FlowLayoutAlgorithm.cs

@@ -72,6 +72,7 @@ namespace Avalonia.Layout
             bool isWrapping,
             double minItemSpacing,
             double lineSpacing,
+            int maxItemsPerLine,
             ScrollOrientation orientation,
             string layoutId)
         {
@@ -94,14 +95,14 @@ namespace Avalonia.Layout
             _elementManager.OnBeginMeasure(orientation);
 
             int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId);
-            Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId);
-            Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId);
+            Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId);
+            Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId);
             if (isWrapping && IsReflowRequired())
             {
                 var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
                 _orientation.SetMinorStart(ref firstElementBounds, 0);
                 _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds);
-                Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId);
+                Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId);
             }
 
             RaiseLineArranged();
@@ -115,10 +116,11 @@ namespace Avalonia.Layout
         public Size Arrange(
             Size finalSize,
             VirtualizingLayoutContext context,
+            bool isWrapping,
             LineAlignment lineAlignment,
             string layoutId)
         {
-            ArrangeVirtualizingLayout(finalSize, lineAlignment, layoutId);
+            ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId);
 
             return new Size(
                 Math.Max(finalSize.Width, _lastExtent.Width),
@@ -270,6 +272,7 @@ namespace Avalonia.Layout
             Size availableSize,
             double minItemSpacing,
             double lineSpacing,
+            int maxItemsPerLine,
             string layoutId)
         {
             if (anchorIndex != -1)
@@ -280,7 +283,7 @@ namespace Avalonia.Layout
                 var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
                 var lineOffset = _orientation.MajorStart(anchorBounds);
                 var lineMajorSize = _orientation.MajorSize(anchorBounds);
-                int countInLine = 1;
+                var countInLine = 1;
                 int count = 0;
                 bool lineNeedsReposition = false;
 
@@ -301,7 +304,7 @@ namespace Avalonia.Layout
                     if (direction == GenerateDirection.Forward)
                     {
                         double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize));
-                        if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
+                        if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
                         {
                             // No more space in this row. wrap to next row.
                             _orientation.SetMinorStart(ref currentBounds, 0);
@@ -339,7 +342,7 @@ namespace Avalonia.Layout
                     {
                         // Backward 
                         double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing);
-                        if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
+                        if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
                         {
                             // Does not fit, wrap to the previous row
                             var availableSizeMinor = _orientation.Minor(availableSize);
@@ -544,6 +547,7 @@ namespace Avalonia.Layout
         private void ArrangeVirtualizingLayout(
             Size finalSize,
             LineAlignment lineAlignment,
+            bool isWrapping,
             string layoutId)
         {
             // Walk through the realized elements one line at a time and 
@@ -563,7 +567,7 @@ namespace Avalonia.Layout
                     if (_orientation.MajorStart(currentBounds) != currentLineOffset)
                     {
                         spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
-                        PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, layoutId);
+                        PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId);
                         spaceAtLineStart = _orientation.MinorStart(currentBounds);
                         countInLine = 0;
                         currentLineOffset = _orientation.MajorStart(currentBounds);
@@ -580,7 +584,7 @@ namespace Avalonia.Layout
                 if (countInLine > 0)
                 {
                     var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
-                    PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, layoutId);
+                    PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId);
                 }
             }
         }
@@ -594,6 +598,8 @@ namespace Avalonia.Layout
             double spaceAtLineEnd,
             double lineSize,
             LineAlignment lineAlignment,
+            bool isWrapping,
+            Size finalSize,
             string layoutId)
         {
             for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex)
@@ -659,6 +665,14 @@ namespace Avalonia.Layout
                 }
 
                 bounds = bounds.Translate(-_lastExtent.Position);
+
+                if (!isWrapping)
+                {
+                    _orientation.SetMinorSize(
+                        ref bounds,
+                        Math.Max(_orientation.MinorSize(bounds), _orientation.Minor(finalSize)));
+                }
+
                 var element = _elementManager.GetAt(rangeIndex);
                 element.Arrange(bounds);
             }

+ 8 - 8
src/Avalonia.Layout/NonVirtualizingLayout.cs

@@ -20,25 +20,25 @@ namespace Avalonia.Layout
         /// <inheritdoc/>
         public sealed override void InitializeForContext(LayoutContext context)
         {
-            InitializeForContextCore((VirtualizingLayoutContext)context);
+            InitializeForContextCore((NonVirtualizingLayoutContext)context);
         }
 
         /// <inheritdoc/>
         public sealed override void UninitializeForContext(LayoutContext context)
         {
-            UninitializeForContextCore((VirtualizingLayoutContext)context);
+            UninitializeForContextCore((NonVirtualizingLayoutContext)context);
         }
 
         /// <inheritdoc/>
         public sealed override Size Measure(LayoutContext context, Size availableSize)
         {
-            return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
+            return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize);
         }
 
         /// <inheritdoc/>
         public sealed override Size Arrange(LayoutContext context, Size finalSize)
         {
-            return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
+            return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize);
         }
 
         /// <summary>
@@ -49,7 +49,7 @@ namespace Avalonia.Layout
         /// The context object that facilitates communication between the layout and its host
         /// container.
         /// </param>
-        protected virtual void InitializeForContextCore(VirtualizingLayoutContext context)
+        protected virtual void InitializeForContextCore(LayoutContext context)
         {
         }
 
@@ -61,7 +61,7 @@ namespace Avalonia.Layout
         /// The context object that facilitates communication between the layout and its host
         /// container.
         /// </param>
-        protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
+        protected virtual void UninitializeForContextCore(LayoutContext context)
         {
         }
 
@@ -83,7 +83,7 @@ namespace Avalonia.Layout
         /// of the allocated sizes for child objects or based on other considerations such as a
         /// fixed container size.
         /// </returns>
-        protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
+        protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize);
 
         /// <summary>
         /// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@@ -98,6 +98,6 @@ namespace Avalonia.Layout
         /// its children.
         /// </param>
         /// <returns>The actual size that is used after the element is arranged in layout.</returns>
-        protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
+        protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize;
     }
 }

+ 14 - 0
src/Avalonia.Layout/NonVirtualizingLayoutContext.cs

@@ -0,0 +1,14 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+namespace Avalonia.Layout
+{
+    /// <summary>
+    /// Represents the base class for layout context types that do not support virtualization.
+    /// </summary>
+    public abstract class NonVirtualizingLayoutContext : LayoutContext
+    {
+    }
+}

+ 2 - 0
src/Avalonia.Layout/StackLayout.cs

@@ -268,6 +268,7 @@ namespace Avalonia.Layout
                 false,
                 0,
                 Spacing,
+                int.MaxValue,
                 _orientation.ScrollOrientation,
                 LayoutId);
 
@@ -279,6 +280,7 @@ namespace Avalonia.Layout
             var value = GetFlowAlgorithm(context).Arrange(
                finalSize,
                context,
+               false,
                FlowLayoutAlgorithm.LineAlignment.Start,
                LayoutId);
 

+ 43 - 11
src/Avalonia.Layout/UniformGridLayout.cs

@@ -111,6 +111,12 @@ namespace Avalonia.Layout
         public static readonly StyledProperty<double> MinRowSpacingProperty =
             AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinRowSpacing));
 
+        /// <summary>
+        /// Defines the <see cref="MaximumRowsOrColumnsProperty"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MaximumRowsOrColumnsProperty =
+            AvaloniaProperty.Register<UniformGridLayout, int>(nameof(MinItemWidth));
+
         /// <summary>
         /// Defines the <see cref="Orientation"/> property.
         /// </summary>
@@ -124,6 +130,7 @@ namespace Avalonia.Layout
         private double _minColumnSpacing;
         private UniformGridLayoutItemsJustification _itemsJustification;
         private UniformGridLayoutItemsStretch _itemsStretch;
+        private int _maximumRowsOrColumns = int.MaxValue;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="UniformGridLayout"/> class.
@@ -220,6 +227,15 @@ namespace Avalonia.Layout
             set => SetValue(MinRowSpacingProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets the maximum row or column count.
+        /// </summary>
+        public int MaximumRowsOrColumns
+        {
+            get => GetValue(MaximumRowsOrColumnsProperty);
+            set => SetValue(MaximumRowsOrColumnsProperty, value);
+        }
+
         /// <summary>
         /// Gets or sets the axis along which items are laid out.
         /// </summary>
@@ -270,15 +286,17 @@ namespace Avalonia.Layout
             {
                 var gridState = (UniformGridLayoutState)context.LayoutState;
                 var lastExtent = gridState.FlowAlgorithm.LastExtent;
-                int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
-                double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context);
-                double realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent);
+                var itemsPerLine = Math.Min( // note use of unsigned ints
+                    Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))),
+                    Math.Max(1u, (uint)_maximumRowsOrColumns));
+                var majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context);
+                var realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent);
                 if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize)
                 {
                     double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent));
                     int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context));
 
-                    anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine));
+                    anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine));
                     bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context);
                 }
             }
@@ -300,7 +318,9 @@ namespace Avalonia.Layout
             int count = context.ItemCount;
             if (targetIndex >= 0 && targetIndex < count)
             {
-                int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
+                int itemsPerLine = (int)Math.Min( // note use of unsigned ints
+                    Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))),
+                    Math.Max(1u, _maximumRowsOrColumns));
                 int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine;
                 index = indexOfFirstInLine;
                 var state = context.LayoutState as UniformGridLayoutState;
@@ -330,17 +350,21 @@ namespace Avalonia.Layout
             // Constants
             int itemsCount = context.ItemCount;
             double availableSizeMinor = _orientation.Minor(availableSize);
-            int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ?
-                (int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount);
+            int itemsPerLine =
+                (int)Math.Min( // note use of unsigned ints
+                    Math.Max(1u, !double.IsInfinity(availableSizeMinor)
+                        ? (uint)(availableSizeMinor / GetMinorSizeWithSpacing(context))
+                        : (uint)itemsCount),
+                Math.Max(1u, _maximumRowsOrColumns));
             double lineSize = GetMajorSizeWithSpacing(context);
 
             if (itemsCount > 0)
             {
                 _orientation.SetMinorSize(
                     ref extent,
-                    !double.IsInfinity(availableSizeMinor) ?
+                    !double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ?
                     availableSizeMinor :
-                    Math.Max(0.0, itemsCount * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing));
+                    Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing));
                 _orientation.SetMajorSize(
                     ref extent,
                     Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing));
@@ -399,7 +423,7 @@ namespace Avalonia.Layout
             // Set the width and height on the grid state. If the user already set them then use the preset. 
             // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items.
             var gridState = (UniformGridLayoutState)context.LayoutState;
-            gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing);
+            gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing, _maximumRowsOrColumns);
 
             var desiredSize = GetFlowAlgorithm(context).Measure(
                 availableSize,
@@ -407,6 +431,7 @@ namespace Avalonia.Layout
                 true,
                 MinItemSpacing,
                 LineSpacing,
+                _maximumRowsOrColumns,
                 _orientation.ScrollOrientation,
                 LayoutId);
 
@@ -422,6 +447,7 @@ namespace Avalonia.Layout
             var value = GetFlowAlgorithm(context).Arrange(
                finalSize,
                context,
+               true,
                (FlowLayoutAlgorithm.LineAlignment)_itemsJustification,
                LayoutId);
             return new Size(value.Width, value.Height);
@@ -474,6 +500,10 @@ namespace Avalonia.Layout
             {
                 _minItemHeight = newValue.GetValueOrDefault<double>();
             }
+            else if (args.Property == MaximumRowsOrColumnsProperty)
+            {
+                _maximumRowsOrColumns = (int)args.NewValue;
+            }
 
             InvalidateLayout();
         }
@@ -502,7 +532,9 @@ namespace Avalonia.Layout
             Rect lastExtent,
             VirtualizingLayoutContext context)
         {
-            int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
+            int itemsPerLine = (int)Math.Min( //note use of unsigned ints
+                Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))),
+                Math.Max(1u, _maximumRowsOrColumns));
             int rowIndex = (int)(index / itemsPerLine);
             int indexInRow = index - (rowIndex * itemsPerLine);
 

+ 26 - 8
src/Avalonia.Layout/UniformGridLayoutState.cs

@@ -48,8 +48,14 @@ namespace Avalonia.Layout
             UniformGridLayoutItemsStretch stretch,
             Orientation orientation,
             double minRowSpacing,
-            double minColumnSpacing)
+            double minColumnSpacing,
+            int maxItemsPerLine)
         {
+            if (maxItemsPerLine == 0)
+            {
+                maxItemsPerLine = 1;
+            }
+
             if (context.ItemCount > 0)
             {
                 // If the first element is realized we don't need to cache it or to get it from the context
@@ -57,7 +63,7 @@ namespace Avalonia.Layout
                 if (realizedElement != null)
                 {
                     realizedElement.Measure(availableSize);
-                    SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing);
+                    SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
                     _cachedFirstElement = null;
                 }
                 else
@@ -72,7 +78,7 @@ namespace Avalonia.Layout
 
                     _cachedFirstElement.Measure(availableSize);
 
-                    SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing);
+                    SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
 
                     // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache.
                     bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement);
@@ -92,8 +98,14 @@ namespace Avalonia.Layout
             UniformGridLayoutItemsStretch stretch,
             Orientation orientation,
             double minRowSpacing,
-            double minColumnSpacing)
+            double minColumnSpacing,
+            int maxItemsPerLine)
         {
+            if (maxItemsPerLine == 0)
+            {
+                maxItemsPerLine = 1;
+            }
+
             EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth);
             EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight);
 
@@ -101,11 +113,17 @@ namespace Avalonia.Layout
             var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing;
 
             var itemSizeMinor = orientation == Orientation.Horizontal ? EffectiveItemWidth : EffectiveItemHeight;
-            itemSizeMinor += minorItemSpacing;
 
-            var numItemsPerColumn = (int)(Math.Max(1.0, availableSizeMinor / itemSizeMinor));
-            var remainingSpace = ((int)availableSizeMinor) % ((int)itemSizeMinor);
-            var extraMinorPixelsForEachItem = remainingSpace / numItemsPerColumn;
+            double extraMinorPixelsForEachItem = 0.0;
+            if (!double.IsInfinity(availableSizeMinor))
+            {
+                var numItemsPerColumn = Math.Min(
+                    maxItemsPerLine,
+                    Math.Max(1.0, availableSizeMinor / (itemSizeMinor + minorItemSpacing)));
+                var usedSpace = (numItemsPerColumn * (itemSizeMinor + minorItemSpacing)) - minorItemSpacing;
+                var remainingSpace = ((int)(availableSizeMinor - usedSpace));
+                extraMinorPixelsForEachItem = remainingSpace / ((int)numItemsPerColumn);
+            }
 
             if (stretch == UniformGridLayoutItemsStretch.Fill)
             {

+ 1 - 1
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -101,7 +101,7 @@ namespace Avalonia.Native
                 .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
                 .Bind<ISystemDialogImpl>().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs()))
                 .Bind<IWindowingPlatformGlFeature>().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature()))
-                .Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows))
+                .Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta))
                 .Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider());
         }
 

+ 6 - 8
src/Avalonia.Styling/Styling/ISetStyleParent.cs → src/Avalonia.Styling/Controls/ISetResourceParent.cs

@@ -1,29 +1,27 @@
-using Avalonia.Controls;
-
-namespace Avalonia.Styling
+namespace Avalonia.Controls
 {
     /// <summary>
-    /// Defines an interface through which a <see cref="Style"/>'s parent can be set.
+    /// Defines an interface through which an <see cref="IResourceNode"/>'s parent can be set.
     /// </summary>
     /// <remarks>
     /// You should not usually need to use this interface - it is for internal use only.
     /// </remarks>
-    public interface ISetStyleParent : IStyle
+    public interface ISetResourceParent : IResourceNode
     {
         /// <summary>
-        /// Sets the style parent.
+        /// Sets the resource parent.
         /// </summary>
         /// <param name="parent">The parent.</param>
         void SetParent(IResourceNode parent);
 
         /// <summary>
-        /// Notifies the style that a change has been made to resources that apply to it.
+        /// Notifies the resource node that a change has been made to the resources in its parent.
         /// </summary>
         /// <param name="e">The event args.</param>
         /// <remarks>
         /// This method will be called automatically by the framework, you should not need to call
         /// this method yourself.
         /// </remarks>
-        void NotifyResourcesChanged(ResourcesChangedEventArgs e);
+        void ParentResourcesChanged(ResourcesChangedEventArgs e);
     }
 }

+ 60 - 2
src/Avalonia.Styling/Controls/ResourceDictionary.cs

@@ -12,8 +12,12 @@ namespace Avalonia.Controls
     /// <summary>
     /// An indexed dictionary of resources.
     /// </summary>
-    public class ResourceDictionary : AvaloniaDictionary<object, object>, IResourceDictionary
+    public class ResourceDictionary : AvaloniaDictionary<object, object>,
+        IResourceDictionary,
+        IResourceNode,
+        ISetResourceParent
     {
+        private IResourceNode _parent;
         private AvaloniaList<IResourceProvider> _mergedDictionaries;
 
         /// <summary>
@@ -39,6 +43,12 @@ namespace Avalonia.Controls
                     _mergedDictionaries.ForEachItem(
                         x =>
                         {
+                            if (x is ISetResourceParent setParent)
+                            {
+                                setParent.SetParent(this);
+                                setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
+                            }
+
                             if (x.HasResources)
                             {
                                 OnResourcesChanged();
@@ -48,11 +58,18 @@ namespace Avalonia.Controls
                         },
                         x =>
                         {
+                            if (x is ISetResourceParent setParent)
+                            {
+                                setParent.SetParent(null);
+                                setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
+                            }
+
                             if (x.HasResources)
                             {
                                 OnResourcesChanged();
                             }
 
+                            (x as ISetResourceParent)?.SetParent(null);
                             x.ResourcesChanged -= MergedDictionaryResourcesChanged;
                         },
                         () => { });
@@ -68,6 +85,27 @@ namespace Avalonia.Controls
             get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false);
         }
 
+        /// <inheritdoc/>
+        IResourceNode IResourceNode.ResourceParent => _parent;
+
+        /// <inheritdoc/>
+        void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            NotifyMergedDictionariesResourcesChanged(e);
+            ResourcesChanged?.Invoke(this, e);
+        }
+
+        /// <inheritdoc/>
+        void ISetResourceParent.SetParent(IResourceNode parent)
+        {
+            if (_parent != null && parent != null)
+            {
+                throw new InvalidOperationException("The ResourceDictionary already has a parent.");
+            }
+            
+            _parent = parent;
+        }
+
         /// <inheritdoc/>
         public bool TryGetResource(object key, out object value)
         {
@@ -95,7 +133,27 @@ namespace Avalonia.Controls
             ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
         }
 
-        private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => OnResourcesChanged();
+        private void NotifyMergedDictionariesResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            if (_mergedDictionaries != null)
+            {
+                for (var i = _mergedDictionaries.Count - 1; i >= 0; --i)
+                {
+                    if (_mergedDictionaries[i] is ISetResourceParent merged)
+                    {
+                        merged.ParentResourcesChanged(e);
+                    }
+                }
+            }
+        }
+
+        private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            var ev = new ResourcesChangedEventArgs();
+            NotifyMergedDictionariesResourcesChanged(ev);
+            OnResourcesChanged();
+        }
+
         private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged();
     }
 }

+ 2 - 2
src/Avalonia.Styling/StyledElement.cs

@@ -223,13 +223,13 @@ namespace Avalonia
                 {
                     if (_styles != null)
                     {
-                        (_styles as ISetStyleParent)?.SetParent(null);
+                        (_styles as ISetResourceParent)?.SetParent(null);
                         _styles.ResourcesChanged -= ThisResourcesChanged;
                     }
 
                     _styles = value;
 
-                    if (value is ISetStyleParent setParent && setParent.ResourceParent == null)
+                    if (value is ISetResourceParent setParent && setParent.ResourceParent == null)
                     {
                         setParent.SetParent(this);
                     }

+ 6 - 6
src/Avalonia.Styling/Styling/Style.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Styling
     /// <summary>
     /// Defines a style.
     /// </summary>
-    public class Style : AvaloniaObject, IStyle, ISetStyleParent
+    public class Style : AvaloniaObject, IStyle, ISetResourceParent
     {
         private static Dictionary<IStyleable, CompositeDisposable> _applied =
             new Dictionary<IStyleable, CompositeDisposable>();
@@ -59,16 +59,16 @@ namespace Avalonia.Styling
 
                 if (_resources != null)
                 {
-                    hadResources = _resources.Count > 0;
+                    hadResources = _resources.HasResources;
                     _resources.ResourcesChanged -= ResourceDictionaryChanged;
                 }
 
                 _resources = value;
                 _resources.ResourcesChanged += ResourceDictionaryChanged;
 
-                if (hadResources || _resources.Count > 0)
+                if (hadResources || _resources.HasResources)
                 {
-                    ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                    ((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs());
                 }
             }
         }
@@ -194,13 +194,13 @@ namespace Avalonia.Styling
         }
 
         /// <inheritdoc/>
-        void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
         {
             ResourcesChanged?.Invoke(this, e);
         }
 
         /// <inheritdoc/>
-        void ISetStyleParent.SetParent(IResourceNode parent)
+        void ISetResourceParent.SetParent(IResourceNode parent)
         {
             if (_parent != null && parent != null)
             {

+ 10 - 10
src/Avalonia.Styling/Styling/Styles.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Styling
     /// <summary>
     /// A style that consists of a number of child styles.
     /// </summary>
-    public class Styles : AvaloniaObject, IAvaloniaList<IStyle>, IStyle, ISetStyleParent
+    public class Styles : AvaloniaObject, IAvaloniaList<IStyle>, IStyle, ISetResourceParent
     {
         private IResourceNode _parent;
         private IResourceDictionary _resources;
@@ -27,10 +27,10 @@ namespace Avalonia.Styling
             _styles.ForEachItem(
                 x =>
                 {
-                    if (x.ResourceParent == null && x is ISetStyleParent setParent)
+                    if (x.ResourceParent == null && x is ISetResourceParent setParent)
                     {
                         setParent.SetParent(this);
-                        setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                        setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
                     }
 
                     if (x.HasResources)
@@ -43,10 +43,10 @@ namespace Avalonia.Styling
                 },
                 x =>
                 {
-                    if (x.ResourceParent == this && x is ISetStyleParent setParent)
+                    if (x.ResourceParent == this && x is ISetResourceParent setParent)
                     {
                         setParent.SetParent(null);
-                        setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                        setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
                     }
 
                     if (x.HasResources)
@@ -98,7 +98,7 @@ namespace Avalonia.Styling
 
                 if (hadResources || _resources.Count > 0)
                 {
-                    ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                    ((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs());
                 }
             }
         }
@@ -246,7 +246,7 @@ namespace Avalonia.Styling
         IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator();
 
         /// <inheritdoc/>
-        void ISetStyleParent.SetParent(IResourceNode parent)
+        void ISetResourceParent.SetParent(IResourceNode parent)
         {
             if (_parent != null && parent != null)
             {
@@ -257,7 +257,7 @@ namespace Avalonia.Styling
         }
 
         /// <inheritdoc/>
-        void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
         {
             ResourcesChanged?.Invoke(this, e);
         }
@@ -266,7 +266,7 @@ namespace Avalonia.Styling
         {
             foreach (var child in this)
             {
-                (child as ISetStyleParent)?.NotifyResourcesChanged(e);
+                (child as ISetResourceParent)?.ParentResourcesChanged(e);
             }
 
             ResourcesChanged?.Invoke(this, e);
@@ -280,7 +280,7 @@ namespace Avalonia.Styling
             {
                 if (foundSource)
                 {
-                    (child as ISetStyleParent)?.NotifyResourcesChanged(e);
+                    (child as ISetResourceParent)?.ParentResourcesChanged(e);
                 }
 
                 foundSource |= child == sender;

+ 4 - 2
src/Avalonia.Visuals/Media/Drawing.cs

@@ -1,4 +1,6 @@
-namespace Avalonia.Media
+using Avalonia.Platform;
+
+namespace Avalonia.Media
 {
     public abstract class Drawing : AvaloniaObject
     {
@@ -6,4 +8,4 @@
 
         public abstract Rect GetBounds();
     }
-}
+}

+ 16 - 5
src/Avalonia.Visuals/Media/DrawingContext.cs

@@ -74,18 +74,29 @@ namespace Avalonia.Media
         public Matrix CurrentContainerTransform => _currentContainerTransform;
 
         /// <summary>
-        /// Draws a bitmap image.
+        /// Draws an image.
         /// </summary>
-        /// <param name="source">The bitmap image.</param>
-        /// <param name="opacity">The opacity to draw with.</param>
+        /// <param name="source">The image.</param>
+        /// <param name="rect">The rect in the output to draw to.</param>
+        public void DrawImage(IImage source, Rect rect)
+        {
+            Contract.Requires<ArgumentNullException>(source != null);
+
+            DrawImage(source, new Rect(source.Size), rect);
+        }
+
+        /// <summary>
+        /// Draws an image.
+        /// </summary>
+        /// <param name="source">The image.</param>
         /// <param name="sourceRect">The rect in the image to draw.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
         /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
-        public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default)
+        public void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default)
         {
             Contract.Requires<ArgumentNullException>(source != null);
 
-            PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect, bitmapInterpolationMode);
+            source.Draw(this, sourceRect, destRect, bitmapInterpolationMode);
         }
 
         /// <summary>

+ 2 - 1
src/Avalonia.Visuals/Media/DrawingGroup.cs

@@ -1,5 +1,6 @@
 using Avalonia.Collections;
 using Avalonia.Metadata;
+using Avalonia.Platform;
 
 namespace Avalonia.Media
 {
@@ -55,4 +56,4 @@ namespace Avalonia.Media
             return rect;
         }
     }
-}
+}

+ 81 - 0
src/Avalonia.Visuals/Media/DrawingImage.cs

@@ -0,0 +1,81 @@
+using System;
+using Avalonia.Metadata;
+using Avalonia.Platform;
+using Avalonia.Visuals.Media.Imaging;
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// An <see cref="IImage"/> that uses a <see cref="Drawing"/> for content.
+    /// </summary>
+    public class DrawingImage : AvaloniaObject, IImage, IAffectsRender
+    {
+        /// <summary>
+        /// Defines the <see cref="Drawing"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Drawing> DrawingProperty =
+            AvaloniaProperty.Register<DrawingImage, Drawing>(nameof(Drawing));
+
+        /// <inheritdoc/>
+        public event EventHandler Invalidated;
+
+        /// <summary>
+        /// Gets or sets the drawing content.
+        /// </summary>
+        [Content]
+        public Drawing Drawing
+        {
+            get => GetValue(DrawingProperty);
+            set => SetValue(DrawingProperty, value);
+        }
+
+        /// <inheritdoc/>
+        public Size Size => Drawing?.GetBounds().Size ?? default;
+
+        /// <inheritdoc/>
+        void IImage.Draw(
+            DrawingContext context,
+            Rect sourceRect,
+            Rect destRect,
+            BitmapInterpolationMode bitmapInterpolationMode)
+        {
+            var drawing = Drawing;
+
+            if (drawing == null)
+            {
+                return;
+            }
+
+            var bounds = drawing.GetBounds();
+            var scale = Matrix.CreateScale(
+                destRect.Width / sourceRect.Width,
+                destRect.Height / sourceRect.Height);
+            var translate = Matrix.CreateTranslation(
+                -sourceRect.X + destRect.X - bounds.X,
+                -sourceRect.Y + destRect.Y - bounds.Y);
+
+            using (context.PushClip(destRect))
+            using (context.PushPreTransform(translate * scale))
+            {
+                Drawing?.Draw(context);
+            }
+        }
+
+        /// <inheritdoc/>
+        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            base.OnPropertyChanged(e);
+
+            if (e.Property == DrawingProperty)
+            {
+                RaiseInvalidated(EventArgs.Empty);
+            }
+        }
+
+        /// <summary>
+        /// Raises the <see cref="Invalidated"/> event.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e);
+    }
+}

+ 4 - 1
src/Avalonia.Visuals/Media/GeometryDrawing.cs

@@ -1,10 +1,13 @@
-namespace Avalonia.Media
+using Avalonia.Metadata;
+
+namespace Avalonia.Media
 {
     public class GeometryDrawing : Drawing
     {
         public static readonly StyledProperty<Geometry> GeometryProperty =
             AvaloniaProperty.Register<GeometryDrawing, Geometry>(nameof(Geometry));
 
+        [Content]
         public Geometry Geometry
         {
             get => GetValue(GeometryProperty);

+ 29 - 0
src/Avalonia.Visuals/Media/IImage.cs

@@ -0,0 +1,29 @@
+using Avalonia.Platform;
+using Avalonia.Visuals.Media.Imaging;
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Represents a raster or vector image.
+    /// </summary>
+    public interface IImage
+    {
+        /// <summary>
+        /// Gets the size of the image, in device independent pixels.
+        /// </summary>
+        Size Size { get; }
+
+        /// <summary>
+        /// Draws the image to a <see cref="DrawingContext"/>.
+        /// </summary>
+        /// <param name="context">The drawing context.</param>
+        /// <param name="sourceRect">The rect in the image to draw.</param>
+        /// <param name="destRect">The rect in the output to draw to.</param>
+        /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
+        void Draw(
+            DrawingContext context,
+            Rect sourceRect,
+            Rect destRect,
+            BitmapInterpolationMode bitmapInterpolationMode);
+    }
+}

+ 20 - 0
src/Avalonia.Visuals/Media/Imaging/Bitmap.cs

@@ -5,6 +5,7 @@ using System;
 using System.IO;
 using Avalonia.Platform;
 using Avalonia.Utilities;
+using Avalonia.Visuals.Media.Imaging;
 
 namespace Avalonia.Media.Imaging
 {
@@ -94,9 +95,28 @@ namespace Avalonia.Media.Imaging
             PlatformImpl.Item.Save(fileName);
         }
 
+        /// <summary>
+        /// Saves the bitmap to a stream.
+        /// </summary>
+        /// <param name="stream">The stream.</param>
         public void Save(Stream stream)
         {
             PlatformImpl.Item.Save(stream);
         }
+
+        /// <inheritdoc/>
+        void IImage.Draw(
+            DrawingContext context,
+            Rect sourceRect,
+            Rect destRect,
+            BitmapInterpolationMode bitmapInterpolationMode)
+        {
+            context.PlatformImpl.DrawBitmap(
+                PlatformImpl,
+                1,
+                sourceRect,
+                destRect,
+                bitmapInterpolationMode);
+        }
     }
 }

+ 1 - 10
src/Avalonia.Visuals/Media/Imaging/IBitmap.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Media.Imaging
     /// <summary>
     /// Represents a bitmap image.
     /// </summary>
-    public interface IBitmap : IDisposable
+    public interface IBitmap : IImage, IDisposable
     {
         /// <summary>
         /// Gets the dots per inch (DPI) of the image.
@@ -32,15 +32,6 @@ namespace Avalonia.Media.Imaging
         /// </summary>
         IRef<IBitmapImpl> PlatformImpl { get; }
 
-        /// <summary>
-        /// Gets the size of the image, in device independent pixels.
-        /// </summary>
-        /// <remarks>
-        /// Note that Skia does not currently support reading the DPI of an image so this value
-        /// will equal <see cref="PixelSize"/> on Skia.
-        /// </remarks>
-        Size Size { get; }
-
         /// <summary>
         /// Saves the bitmap to a file.
         /// </summary>

+ 77 - 13
src/Avalonia.Visuals/Media/MediaExtensions.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using Avalonia.Utilities;
 
 namespace Avalonia.Media
 {
@@ -16,24 +17,82 @@ namespace Avalonia.Media
         /// <param name="stretch">The stretch mode.</param>
         /// <param name="destinationSize">The size of the destination viewport.</param>
         /// <param name="sourceSize">The size of the source.</param>
+        /// <param name="stretchDirection">The stretch direction.</param>
         /// <returns>A vector with the X and Y scaling factors.</returns>
-        public static Vector CalculateScaling(this Stretch stretch, Size destinationSize, Size sourceSize)
+        public static Vector CalculateScaling(
+            this Stretch stretch,
+            Size destinationSize,
+            Size sourceSize,
+            StretchDirection stretchDirection = StretchDirection.Both)
         {
-            double scaleX = 1;
-            double scaleY = 1;
+            var scaleX = 1.0;
+            var scaleY = 1.0;
 
-            if (stretch != Stretch.None)
+            bool isConstrainedWidth = !double.IsPositiveInfinity(destinationSize.Width);
+            bool isConstrainedHeight = !double.IsPositiveInfinity(destinationSize.Height);
+
+            if ((stretch == Stretch.Uniform || stretch == Stretch.UniformToFill || stretch == Stretch.Fill)
+                 && (isConstrainedWidth || isConstrainedHeight))
             {
-                scaleX = destinationSize.Width / sourceSize.Width;
-                scaleY = destinationSize.Height / sourceSize.Height;
+                // Compute scaling factors for both axes
+                scaleX = MathUtilities.IsZero(sourceSize.Width) ? 0.0 : destinationSize.Width / sourceSize.Width;
+                scaleY = MathUtilities.IsZero(sourceSize.Height) ? 0.0 : destinationSize.Height / sourceSize.Height;
 
-                switch (stretch)
+                if (!isConstrainedWidth)
+                {
+                    scaleX = scaleY;
+                }
+                else if (!isConstrainedHeight)
+                {
+                    scaleY = scaleX;
+                }
+                else
                 {
-                    case Stretch.Uniform:
-                        scaleX = scaleY = Math.Min(scaleX, scaleY);
+                    // If not preserving aspect ratio, then just apply transform to fit
+                    switch (stretch)
+                    {
+                        case Stretch.Uniform:
+                            // Find minimum scale that we use for both axes
+                            double minscale = scaleX < scaleY ? scaleX : scaleY;
+                            scaleX = scaleY = minscale;
+                            break;
+
+                        case Stretch.UniformToFill:
+                            // Find maximum scale that we use for both axes
+                            double maxscale = scaleX > scaleY ? scaleX : scaleY;
+                            scaleX = scaleY = maxscale;
+                            break;
+
+                        case Stretch.Fill:
+                            // We already computed the fill scale factors above, so just use them
+                            break;
+                    }
+                }
+
+                // Apply stretch direction by bounding scales.
+                // In the uniform case, scaleX=scaleY, so this sort of clamping will maintain aspect ratio
+                // In the uniform fill case, we have the same result too.
+                // In the fill case, note that we change aspect ratio, but that is okay
+                switch (stretchDirection)
+                {
+                    case StretchDirection.UpOnly:
+                        if (scaleX < 1.0)
+                            scaleX = 1.0;
+                        if (scaleY < 1.0)
+                            scaleY = 1.0;
+                        break;
+
+                    case StretchDirection.DownOnly:
+                        if (scaleX > 1.0)
+                            scaleX = 1.0;
+                        if (scaleY > 1.0)
+                            scaleY = 1.0;
                         break;
-                    case Stretch.UniformToFill:
-                        scaleX = scaleY = Math.Max(scaleX, scaleY);
+
+                    case StretchDirection.Both:
+                        break;
+
+                    default:
                         break;
                 }
             }
@@ -47,10 +106,15 @@ namespace Avalonia.Media
         /// <param name="stretch">The stretch mode.</param>
         /// <param name="destinationSize">The size of the destination viewport.</param>
         /// <param name="sourceSize">The size of the source.</param>
+        /// <param name="stretchDirection">The stretch direction.</param>
         /// <returns>The size of the stretched source.</returns>
-        public static Size CalculateSize(this Stretch stretch, Size destinationSize, Size sourceSize)
+        public static Size CalculateSize(
+            this Stretch stretch,
+            Size destinationSize,
+            Size sourceSize,
+            StretchDirection stretchDirection = StretchDirection.Both)
         {
-            return sourceSize * stretch.CalculateScaling(destinationSize, sourceSize);
+            return sourceSize * stretch.CalculateScaling(destinationSize, sourceSize, stretchDirection);
         }
     }
 }

+ 25 - 0
src/Avalonia.Visuals/Media/StretchDirection.cs

@@ -0,0 +1,25 @@
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Describes the type of scaling that can be used when scaling content.
+    /// </summary>
+    public enum StretchDirection
+    {
+        /// <summary>
+        /// Only scales the content upwards when the content is smaller than the available space.
+        /// If the content is larger, no scaling downwards is done.
+        /// </summary>
+        UpOnly,
+
+        /// <summary>
+        /// Only scales the content downwards when the content is larger than the available space.
+        /// If the content is smaller, no scaling upwards is done.
+        /// </summary>
+        DownOnly,
+
+        /// <summary>
+        /// Always stretches to fit the available space according to the stretch mode.
+        /// </summary>
+        Both,
+    }
+}

+ 2 - 2
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@@ -33,7 +33,7 @@ namespace Avalonia.Platform
         /// <param name="sourceRect">The rect in the image to draw.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
         /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
-        void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default);
+        void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default);
 
         /// <summary>
         /// Draws a bitmap image.
@@ -42,7 +42,7 @@ namespace Avalonia.Platform
         /// <param name="opacityMask">The opacity mask to draw with.</param>
         /// <param name="opacityMaskRect">The destination rect for the opacity mask.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
-        void DrawImage(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect);
+        void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect);
 
         /// <summary>
         /// Draws a line.

+ 3 - 3
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -469,11 +469,11 @@ namespace Avalonia.Rendering
 
                 if (layer.OpacityMask == null)
                 {
-                    context.DrawImage(bitmap, layer.Opacity, sourceRect, clientRect);
+                    context.DrawBitmap(bitmap, layer.Opacity, sourceRect, clientRect);
                 }
                 else
                 {
-                    context.DrawImage(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect);
+                    context.DrawBitmap(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect);
                 }
 
                 if (layer.GeometryClip != null)
@@ -485,7 +485,7 @@ namespace Avalonia.Rendering
             if (_overlay != null)
             {
                 var sourceRect = new Rect(0, 0, _overlay.Item.PixelSize.Width, _overlay.Item.PixelSize.Height);
-                context.DrawImage(_overlay, 0.5, sourceRect, clientRect);
+                context.DrawBitmap(_overlay, 0.5, sourceRect, clientRect);
             }
 
             if (DrawFps)

+ 3 - 1
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@@ -307,7 +307,9 @@ namespace Avalonia.Rendering
 
                         if (!child.ClipToBounds || clipRect.Intersects(childBounds))
                         {
-                            var childClipRect = clipRect.Translate(-childBounds.Position);
+                            var childClipRect = child.RenderTransform == null
+                                ? clipRect.Translate(-childBounds.Position)
+                                : clipRect;
                             Render(context, child, childClipRect);
                         }
                         else

+ 2 - 2
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@@ -115,7 +115,7 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
+        public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
         {
             var next = NextDrawAs<ImageNode>();
 
@@ -130,7 +130,7 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public void DrawImage(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect)
+        public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect)
         {
             // This method is currently only used to composite layers so shouldn't be called here.
             throw new NotSupportedException();

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs

@@ -100,7 +100,7 @@ namespace Avalonia.Rendering.SceneGraph
         public override void Render(IDrawingContextImpl context)
         {
             context.Transform = Transform;
-            context.DrawImage(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode);
+            context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode);
         }
 
         /// <inheritdoc/>

+ 1 - 1
src/Avalonia.X11/X11IconLoader.cs

@@ -59,7 +59,7 @@ namespace Avalonia.X11
             }
             using(var rt = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>().CreateRenderTarget(new[]{this}))
             using (var ctx = rt.CreateDrawingContext(null))
-                ctx.DrawImage(bitmap.PlatformImpl, 1, new Rect(bitmap.Size),
+                ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size),
                     new Rect(0, 0, _width, _height));
             Data = new UIntPtr[_width * _height + 2];
             Data[0] = new UIntPtr((uint)_width);

+ 1 - 1
src/Avalonia.X11/X11Platform.cs

@@ -44,7 +44,7 @@ namespace Avalonia.X11
                 .Bind<IPlatformThreadingInterface>().ToConstant(new X11PlatformThreading(this))
                 .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
-                .Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Control))
+                .Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control))
                 .Bind<IKeyboardDevice>().ToFunc(() => KeyboardDevice)
                 .Bind<IStandardCursorFactory>().ToConstant(new X11CursorFactory(Display))
                 .Bind<IClipboard>().ToConstant(new X11Clipboard(this))

+ 25 - 1
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs

@@ -7,8 +7,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
     /// <summary>
     /// Loads a resource dictionary from a specified URL.
     /// </summary>
-    public class ResourceInclude :IResourceProvider
+    public class ResourceInclude : IResourceNode, ISetResourceParent
     {
+        private IResourceNode _parent;
         private Uri _baseUri;
         private IResourceDictionary _loaded;
 
@@ -26,6 +27,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
                     var loader = new AvaloniaXamlLoader();
                     _loaded = (IResourceDictionary)loader.Load(Source, _baseUri);
 
+                    (_loaded as ISetResourceParent)?.SetParent(this);
+                    _loaded.ResourcesChanged += ResourcesChanged;
+
                     if (_loaded.HasResources)
                     {
                         ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
@@ -44,12 +48,32 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
         /// <inhertidoc/>
         bool IResourceProvider.HasResources => Loaded.HasResources;
 
+        /// <inhertidoc/>
+        IResourceNode IResourceNode.ResourceParent => _parent;
+
         /// <inhertidoc/>
         bool IResourceProvider.TryGetResource(object key, out object value)
         {
             return Loaded.TryGetResource(key, out value);
         }
 
+        /// <inhertidoc/>
+        void ISetResourceParent.SetParent(IResourceNode parent)
+        {
+            if (_parent != null && parent != null)
+            {
+                throw new InvalidOperationException("The ResourceInclude already has a parent.");
+            }
+
+            _parent = parent;
+        }
+
+        /// <inhertidoc/>
+        void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            (_loaded as ISetResourceParent)?.ParentResourcesChanged(e);
+        }
+
         public ResourceInclude ProvideValue(IServiceProvider serviceProvider)
         {
             var tdc = (ITypeDescriptorContext)serviceProvider;

+ 5 - 5
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Styling
     /// <summary>
     /// Includes a style from a URL.
     /// </summary>
-    public class StyleInclude : IStyle, ISetStyleParent
+    public class StyleInclude : IStyle, ISetResourceParent
     {
         private Uri _baseUri;
         private IStyle _loaded;
@@ -53,7 +53,7 @@ namespace Avalonia.Markup.Xaml.Styling
                 {
                     var loader = new AvaloniaXamlLoader();
                     _loaded = (IStyle)loader.Load(Source, _baseUri);
-                    (_loaded as ISetStyleParent)?.SetParent(this);
+                    (_loaded as ISetResourceParent)?.SetParent(this);
                 }
 
                 return _loaded;
@@ -89,13 +89,13 @@ namespace Avalonia.Markup.Xaml.Styling
         public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value);
 
         /// <inheritdoc/>
-        void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
         {
-            (Loaded as ISetStyleParent)?.NotifyResourcesChanged(e);
+            (Loaded as ISetResourceParent)?.ParentResourcesChanged(e);
         }
 
         /// <inheritdoc/>
-        void ISetStyleParent.SetParent(IResourceNode parent)
+        void ISetResourceParent.SetParent(IResourceNode parent)
         {
             if (_parent != null && parent != null)
             {

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs

@@ -99,7 +99,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                 void Add(string type, string conv)
                     => AddType(typeSystem.GetType(type), typeSystem.GetType(conv));
                 
-                Add("Avalonia.Media.Imaging.IBitmap","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter");
+                Add("Avalonia.Media.IImage","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter");
                 var ilist = typeSystem.GetType("System.Collections.Generic.IList`1");
                 AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")),
                     typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter"));

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

@@ -110,7 +110,7 @@ namespace Avalonia.Skia
         }
 
         /// <inheritdoc />
-        public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
+        public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
         {
             var drawableImage = (IDrawableBitmapImpl)source.Item;
             var s = sourceRect.ToSKRect();
@@ -146,10 +146,10 @@ namespace Avalonia.Skia
         }
 
         /// <inheritdoc />
-        public void DrawImage(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
+        public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
         {
             PushOpacityMask(opacityMask, opacityMaskRect);
-            DrawImage(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default);
+            DrawBitmap(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default);
             PopOpacityMask();
         }
 
@@ -437,7 +437,7 @@ namespace Avalonia.Skia
                 context.Clear(Colors.Transparent);
                 context.PushClip(calc.IntermediateClip);
                 context.Transform = calc.IntermediateTransform;
-                context.DrawImage(
+                context.DrawBitmap(
                     RefCountable.CreateUnownedNotClonable(tileBrushImage),
                     1,
                     sourceRect,

+ 4 - 1
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@@ -89,12 +89,15 @@ namespace Avalonia.Skia
 
             if (typeface.FontFamily.Key == null)
             {
+                var defaultName = SKTypeface.Default.FamilyName;
+
                 foreach (var familyName in typeface.FontFamily.FamilyNames)
                 {
                     skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)typeface.Weight,
                         SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
 
-                    if (skTypeface == SKTypeface.Default)
+                    if (!skTypeface.FamilyName.Equals(familyName, StringComparison.Ordinal) &&
+                        defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal))
                     {
                         continue;
                     }

+ 2 - 2
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -109,7 +109,7 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="sourceRect">The rect in the image to draw.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
         /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
-        public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
+        public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
         {
             using (var d2d = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext))
             {
@@ -149,7 +149,7 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="opacityMask">The opacity mask to draw with.</param>
         /// <param name="opacityMaskRect">The destination rect for the opacity mask.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
-        public void DrawImage(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
+        public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
         {
             using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext))
             using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value))

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs

@@ -107,7 +107,7 @@ namespace Avalonia.Direct2D1.Media
                 context.PushClip(calc.IntermediateClip);
                 context.Transform = calc.IntermediateTransform;
                 
-                context.DrawImage(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect, _bitmapInterpolationMode);
+                context.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect, _bitmapInterpolationMode);
                 context.PopClip();
             }
 

+ 2 - 1
src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs

@@ -30,7 +30,7 @@ namespace Avalonia.Direct2D1.Media
             _direct2DBitmap = d2DBitmap ?? throw new ArgumentNullException(nameof(d2DBitmap));
         }
 
-        public override Vector Dpi => _direct2DBitmap.DotsPerInch.ToAvaloniaVector();
+        public override Vector Dpi => new Vector(96, 96);
         public override PixelSize PixelSize => _direct2DBitmap.PixelSize.ToAvalonia();
 
         public override void Dispose()
@@ -58,3 +58,4 @@ namespace Avalonia.Direct2D1.Media
         }
     }
 }
+;

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs

@@ -58,7 +58,7 @@ namespace Avalonia.Direct2D1.Media.Imaging
             {
                 using (var dc = wic.CreateDrawingContext(null))
                 {
-                    dc.DrawImage(
+                    dc.DrawBitmap(
                         RefCountable.CreateUnownedNotClonable(this),
                         1,
                         new Rect(PixelSize.ToSizeWithDpi(Dpi.X)),

+ 6 - 8
src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs

@@ -26,6 +26,7 @@ namespace Avalonia.Direct2D1.Media
             using (BitmapDecoder decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, fileName, DecodeOptions.CacheOnDemand))
             {
                 WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand);
+                Dpi = new Vector(96, 96);
             }
         }
 
@@ -39,6 +40,7 @@ namespace Avalonia.Direct2D1.Media
             _decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, stream, DecodeOptions.CacheOnLoad);
 
             WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, _decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnLoad);
+            Dpi = new Vector(96, 96);
         }
 
         /// <summary>
@@ -62,6 +64,7 @@ namespace Avalonia.Direct2D1.Media
                 pixelFormat.Value.ToWic(),
                 BitmapCreateCacheOption.CacheOnLoad);
             WicImpl.SetResolution(dpi.X, dpi.Y);
+            Dpi = dpi;
         }
 
         public WicBitmapImpl(APixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride)
@@ -70,6 +73,8 @@ namespace Avalonia.Direct2D1.Media
             WicImpl.SetResolution(dpi.X, dpi.Y);
 
             PixelFormat = format;
+            Dpi = dpi;
+
             using (var l = WicImpl.Lock(BitmapLockFlags.Write))
             {
                 for (var row = 0; row < size.Height; row++)
@@ -82,14 +87,7 @@ namespace Avalonia.Direct2D1.Media
             }
         }
 
-        public override Vector Dpi
-        {
-            get
-            {
-                WicImpl.GetResolution(out double x, out double y);
-                return new Vector(x, y);
-            }
-        }
+        public override Vector Dpi { get; }
 
         public override PixelSize PixelSize => WicImpl.Size.ToAvalonia();
 

+ 62 - 8
tests/Avalonia.Controls.UnitTests/ImageTests.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Measure_Should_Return_Correct_Size_For_No_Stretch()
         {
-            var bitmap = Mock.Of<IBitmap>(x => x.PixelSize == new PixelSize(50, 100));
+            var bitmap = CreateBitmap(50, 100);
             var target = new Image();
             target.Stretch = Stretch.None;
             target.Source = bitmap;
@@ -26,7 +26,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Measure_Should_Return_Correct_Size_For_Fill_Stretch()
         {
-            var bitmap = Mock.Of<IBitmap>(x => x.PixelSize == new PixelSize(50, 100));
+            var bitmap = CreateBitmap(50, 100);
             var target = new Image();
             target.Stretch = Stretch.Fill;
             target.Source = bitmap;
@@ -39,7 +39,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Measure_Should_Return_Correct_Size_For_Uniform_Stretch()
         {
-            var bitmap = Mock.Of<IBitmap>(x => x.PixelSize == new PixelSize(50, 100));
+            var bitmap = CreateBitmap(50, 100);
             var target = new Image();
             target.Stretch = Stretch.Uniform;
             target.Source = bitmap;
@@ -52,7 +52,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Measure_Should_Return_Correct_Size_For_UniformToFill_Stretch()
         {
-            var bitmap = Mock.Of<IBitmap>(x => x.PixelSize == new PixelSize(50, 100));
+            var bitmap = CreateBitmap(50, 100);
             var target = new Image();
             target.Stretch = Stretch.UniformToFill;
             target.Source = bitmap;
@@ -62,10 +62,59 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(new Size(50, 50), target.DesiredSize);
         }
 
+        [Fact]
+        public void Measure_Should_Return_Correct_Size_With_StretchDirection_DownOnly()
+        {
+            var bitmap = CreateBitmap(50, 100);
+            var target = new Image();
+            target.StretchDirection = StretchDirection.DownOnly;
+            target.Source = bitmap;
+
+            target.Measure(new Size(150, 150));
+
+            Assert.Equal(new Size(50, 100), target.DesiredSize);
+        }
+
+        [Fact]
+        public void Measure_Should_Return_Correct_Size_For_Infinite_Height()
+        {
+            var bitmap = CreateBitmap(50, 100);
+            var image = new Image();
+            image.Source = bitmap;
+
+            image.Measure(new Size(200, double.PositiveInfinity));
+
+            Assert.Equal(new Size(200, 400), image.DesiredSize);
+        }
+
+        [Fact]
+        public void Measure_Should_Return_Correct_Size_For_Infinite_Width()
+        {
+            var bitmap = CreateBitmap(50, 100);
+            var image = new Image();
+            image.Source = bitmap;
+
+            image.Measure(new Size(double.PositiveInfinity, 400));
+
+            Assert.Equal(new Size(200, 400), image.DesiredSize);
+        }
+
+        [Fact]
+        public void Measure_Should_Return_Correct_Size_For_Infinite_Width_Height()
+        {
+            var bitmap = CreateBitmap(50, 100);
+            var image = new Image();
+            image.Source = bitmap;
+
+            image.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+
+            Assert.Equal(new Size(50, 100), image.DesiredSize);
+        }
+
         [Fact]
         public void Arrange_Should_Return_Correct_Size_For_No_Stretch()
         {
-            var bitmap = Mock.Of<IBitmap>(x => x.PixelSize == new PixelSize(50, 100));
+            var bitmap = CreateBitmap(50, 100);
             var target = new Image();
             target.Stretch = Stretch.None;
             target.Source = bitmap;
@@ -79,7 +128,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Arrange_Should_Return_Correct_Size_For_Fill_Stretch()
         {
-            var bitmap = Mock.Of<IBitmap>(x => x.PixelSize == new PixelSize(50, 100));
+            var bitmap = CreateBitmap(50, 100);
             var target = new Image();
             target.Stretch = Stretch.Fill;
             target.Source = bitmap;
@@ -93,7 +142,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Arrange_Should_Return_Correct_Size_For_Uniform_Stretch()
         {
-            var bitmap = Mock.Of<IBitmap>(x => x.PixelSize == new PixelSize(50, 100));
+            var bitmap = CreateBitmap(50, 100);
             var target = new Image();
             target.Stretch = Stretch.Uniform;
             target.Source = bitmap;
@@ -107,7 +156,7 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Arrange_Should_Return_Correct_Size_For_UniformToFill_Stretch()
         {
-            var bitmap = Mock.Of<IBitmap>(x => x.PixelSize == new PixelSize(50, 100));
+            var bitmap = CreateBitmap(50, 100);
             var target = new Image();
             target.Stretch = Stretch.UniformToFill;
             target.Source = bitmap;
@@ -117,5 +166,10 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.Equal(new Size(25, 100), target.Bounds.Size);
         }
+
+        private IBitmap CreateBitmap(int width, int height)
+        {
+            return Mock.Of<IBitmap>(x => x.Size == new Size(width, height));
+        }
     }
 }

+ 24 - 0
tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs

@@ -36,6 +36,30 @@ namespace Avalonia.Direct2D1.UnitTests.Media
             }
         }
 
+        [Fact]
+        public void Should_Create_Typeface_From_Fallback_Bold()
+        {
+            using (AvaloniaLocator.EnterScope())
+            {
+                Direct2D1Platform.Initialize();
+
+                var fontManager = new FontManagerImpl();
+
+                var defaultName = fontManager.GetDefaultFontFamilyName();
+
+                var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
+                    new Typeface(new FontFamily("A, B, Arial"), FontWeight.Bold));
+
+                var font = glyphTypeface.DWFont;
+
+                Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0));
+
+                Assert.Equal(SharpDX.DirectWrite.FontWeight.Bold, font.Weight);
+
+                Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
+            }
+        }
+
         [Fact]
         public void Should_Create_Typeface_For_Unknown_Font()
         {

+ 123 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs

@@ -0,0 +1,123 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Media;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Xaml
+{
+    public class ResourceDictionaryTests : XamlTestBase
+    {
+        [Fact]
+        public void StaticResource_Works_In_ResourceDictionary()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<ResourceDictionary xmlns='https://github.com/avaloniaui'
+                    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+  <Color x:Key='Red'>Red</Color>
+  <SolidColorBrush x:Key='RedBrush' Color='{StaticResource Red}'/>
+</ResourceDictionary>";
+                var loader = new AvaloniaXamlLoader();
+                var resources = (ResourceDictionary)loader.Load(xaml);
+                var brush = (SolidColorBrush)resources["RedBrush"];
+
+                Assert.Equal(Colors.Red, brush.Color);
+            }
+        }
+
+        [Fact]
+        public void DynamicResource_Works_In_ResourceDictionary()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<ResourceDictionary xmlns='https://github.com/avaloniaui'
+                    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+  <Color x:Key='Red'>Red</Color>
+  <SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
+</ResourceDictionary>";
+                var loader = new AvaloniaXamlLoader();
+                var resources = (ResourceDictionary)loader.Load(xaml);
+                var brush = (SolidColorBrush)resources["RedBrush"];
+
+                Assert.Equal(Colors.Red, brush.Color);
+            }
+        }
+
+        [Fact]
+        public void DynamicResource_Finds_Resource_In_Parent_Dictionary()
+        {
+            var dictionaryXaml = @"
+<ResourceDictionary xmlns='https://github.com/avaloniaui'
+                    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+  <SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
+</ResourceDictionary>";
+
+            using (StyledWindow(assets: ("test:dict.xaml", dictionaryXaml)))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Resources>
+        <ResourceDictionary>
+            <ResourceDictionary.MergedDictionaries>
+                <ResourceInclude Source='test:dict.xaml'/>
+            </ResourceDictionary.MergedDictionaries>
+        </ResourceDictionary>
+        <Color x:Key='Red'>Red</Color>
+    </Window.Resources>
+    <Button Name='button' Background='{DynamicResource RedBrush}'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+
+                var brush = Assert.IsType<SolidColorBrush>(button.Background);
+                Assert.Equal(Colors.Red, brush.Color);
+
+                window.Resources["Red"] = Colors.Green;
+
+                Assert.Equal(Colors.Green, brush.Color);
+            }
+        }
+
+        private IDisposable StyledWindow(params (string, string)[] assets)
+        {
+            var services = TestServices.StyledWindow.With(
+                assetLoader: new MockAssetLoader(assets),
+                theme: () => new Styles
+                {
+                    WindowStyle(),
+                });
+
+            return UnitTestApplication.Start(services);
+        }
+
+        private Style WindowStyle()
+        {
+            return new Style(x => x.OfType<Window>())
+            {
+                Setters =
+                {
+                    new Setter(
+                        Window.TemplateProperty,
+                        new FuncControlTemplate<Window>((x, scope) =>
+                            new ContentPresenter
+                            {
+                                Name = "PART_ContentPresenter",
+                                [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty],
+                            }.RegisterInNameScope(scope)))
+                }
+            };
+        }
+    }
+}

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

@@ -94,7 +94,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                     ctx.DrawRectangle(Brushes.Pink, null, new Rect(0, 20, 100, 10));
 
                     var rc = new Rect(0, 0, 60, 60);
-                    ctx.DrawImage(bmp.PlatformImpl, 1, rc, rc);
+                    ctx.DrawBitmap(bmp.PlatformImpl, 1, rc, rc);
                 }
                 rtb.Save(System.IO.Path.Combine(OutputPath, testName + ".out.png"));
             }

+ 18 - 0
tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Linq;
 using System.Reflection;
 using Avalonia.Media;
 using Avalonia.Platform;
@@ -29,6 +30,23 @@ namespace Avalonia.Skia.UnitTests
             Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
         }
 
+        [Fact]
+        public void Should_Create_Typeface_From_Fallback_Bold()
+        {
+            var fontManager = new FontManagerImpl();
+
+            //we need to have a valid font name different from the default one
+            string fontName = fontManager.GetInstalledFontFamilyNames().First();
+
+            var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
+                new Typeface(new FontFamily($"A, B, {fontName}"), FontWeight.Bold));
+
+            var skTypeface = glyphTypeface.Typeface;
+
+            Assert.Equal(fontName, skTypeface.FamilyName);
+            Assert.Equal(SKFontStyle.Bold.Weight, skTypeface.FontWeight);
+        }
+
         [Fact]
         public void Should_Create_Typeface_For_Unknown_Font()
         {

+ 30 - 15
tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs

@@ -3,6 +3,7 @@
 
 using System;
 using Avalonia.Controls;
+using Moq;
 using Xunit;
 
 namespace Avalonia.Styling.UnitTests
@@ -136,7 +137,7 @@ namespace Avalonia.Styling.UnitTests
         }
 
         [Fact]
-        public void ResourcesChanged_Should_Not_Be_Raised_On_Empty_MergedDictionary_Remove()
+        public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Resource_Add()
         {
             var target = new ResourceDictionary
             {
@@ -145,31 +146,45 @@ namespace Avalonia.Styling.UnitTests
                     new ResourceDictionary(),
                 }
             };
+
             var raised = false;
 
             target.ResourcesChanged += (_, __) => raised = true;
-            target.MergedDictionaries.RemoveAt(0);
+            ((IResourceDictionary)target.MergedDictionaries[0]).Add("foo", "bar");
 
-            Assert.False(raised);
+            Assert.True(raised);
         }
 
         [Fact]
-        public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Resource_Add()
+        public void MergedDictionary_ParentResourcesChanged_Should_Be_Called_On_Resource_Add()
         {
-            var target = new ResourceDictionary
-            {
-                MergedDictionaries =
-                {
-                    new ResourceDictionary(),
-                }
-            };
+            var target = new ResourceDictionary();
+            var merged = new Mock<ISetResourceParent>();
 
-            var raised = false;
+            target.MergedDictionaries.Add(merged.Object);
+            merged.ResetCalls();
 
-            target.ResourcesChanged += (_, __) => raised = true;
-            ((IResourceDictionary)target.MergedDictionaries[0]).Add("foo", "bar");
+            target.Add("foo", "bar");
 
-            Assert.True(raised);
+            merged.Verify(
+                x => x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
+                Times.Once);
+        }
+
+        [Fact]
+        public void MergedDictionary_ParentResourcesChanged_Should_Be_Called_On_NotifyResourceChanged()
+        {
+            var target = new ResourceDictionary();
+            var merged = new Mock<ISetResourceParent>();
+
+            target.MergedDictionaries.Add(merged.Object);
+            merged.ResetCalls();
+
+            ((ISetResourceParent)target).ParentResourcesChanged(new ResourcesChangedEventArgs());
+
+            merged.Verify(
+                x => x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
+                Times.Once);
         }
     }
 }

+ 1 - 1
tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs

@@ -670,7 +670,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
             var context = Mock.Get(target.RenderTarget.CreateDrawingContext(null));
             var borderLayer = target.Layers[border].Bitmap;
 
-            context.Verify(x => x.DrawImage(borderLayer, 0.5, It.IsAny<Rect>(), It.IsAny<Rect>(), BitmapInterpolationMode.Default));
+            context.Verify(x => x.DrawBitmap(borderLayer, 0.5, It.IsAny<Rect>(), It.IsAny<Rect>(), BitmapInterpolationMode.Default));
         }
 
         [Fact]

+ 136 - 0
tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs

@@ -178,5 +178,141 @@ namespace Avalonia.Visuals.UnitTests.Rendering
 
             Assert.Equal(1, rendered);
         }
+
+        [Fact]
+        public void Should_Not_Clip_Children_With_RenderTransform_When_In_Bounds()
+        {
+            const int RootWidth = 300;
+            const int RootHeight = 300;
+
+            var rootGrid = new Grid
+            {
+                Width = RootWidth,
+                Height = RootHeight,
+                ClipToBounds = true
+            };
+
+            var stackPanel = new StackPanel
+            {
+                Orientation = Orientation.Horizontal,
+                VerticalAlignment = VerticalAlignment.Top,
+                HorizontalAlignment = HorizontalAlignment.Right,
+                Margin = new Thickness(0, 10, 0, 0),
+                RenderTransformOrigin = new RelativePoint(new Point(0, 0), RelativeUnit.Relative),
+                RenderTransform = new TransformGroup
+                {
+                    Children =
+                    {
+                        new RotateTransform { Angle = 90 },
+                        new TranslateTransform { X = 240 }
+                    }
+                }
+            };
+
+            rootGrid.Children.Add(stackPanel);
+
+            TestControl CreateControl()
+                => new TestControl
+                {
+                    Width = 80,
+                    Height = 40,
+                    Margin = new Thickness(0, 0, 5, 0),
+                    ClipToBounds = true
+                };
+
+            var control1 = CreateControl();
+            var control2 = CreateControl();
+            var control3 = CreateControl();
+
+            stackPanel.Children.Add(control1);
+            stackPanel.Children.Add(control2);
+            stackPanel.Children.Add(control3);
+
+            var root = new TestRoot(rootGrid);
+            root.Renderer = new ImmediateRenderer(root);
+            root.LayoutManager.ExecuteInitialLayoutPass(root);
+
+            var rootSize = new Size(RootWidth, RootHeight);
+            root.Measure(rootSize);
+            root.Arrange(new Rect(rootSize));
+
+            root.Renderer.Paint(root.Bounds);
+
+            Assert.True(control1.Rendered);
+            Assert.True(control2.Rendered);
+            Assert.True(control3.Rendered);
+        }
+
+        [Fact]
+        public void Should_Not_Render_Clipped_Child_With_RenderTransform_When_Not_In_Bounds()
+        {
+            const int RootWidth = 300;
+            const int RootHeight = 300;
+
+            var rootGrid = new Grid
+            {
+                Width = RootWidth,
+                Height = RootHeight,
+                ClipToBounds = true
+            };
+
+            var stackPanel = new StackPanel
+            {
+                Orientation = Orientation.Horizontal,
+                VerticalAlignment = VerticalAlignment.Top,
+                HorizontalAlignment = HorizontalAlignment.Right,
+                Margin = new Thickness(0, 10, 0, 0),
+                RenderTransformOrigin = new RelativePoint(new Point(0, 0), RelativeUnit.Relative),
+                RenderTransform = new TransformGroup
+                {
+                    Children =
+                    {
+                        new RotateTransform { Angle = 90 },
+                        new TranslateTransform { X = 280 }
+                    }
+                }
+            };
+
+            rootGrid.Children.Add(stackPanel);
+
+            TestControl CreateControl()
+                => new TestControl
+                {
+                    Width = 160,
+                    Height = 40,
+                    Margin = new Thickness(0, 0, 5, 0),
+                    ClipToBounds = true
+                };
+
+            var control1 = CreateControl();
+            var control2 = CreateControl();
+            var control3 = CreateControl();
+
+            stackPanel.Children.Add(control1);
+            stackPanel.Children.Add(control2);
+            stackPanel.Children.Add(control3);
+
+            var root = new TestRoot(rootGrid);
+            root.Renderer = new ImmediateRenderer(root);
+            root.LayoutManager.ExecuteInitialLayoutPass(root);
+
+            var rootSize = new Size(RootWidth, RootHeight);
+            root.Measure(rootSize);
+            root.Arrange(new Rect(rootSize));
+
+            root.Renderer.Paint(root.Bounds);
+
+            Assert.True(control1.Rendered);
+            Assert.True(control2.Rendered);
+            Assert.False(control3.Rendered);
+        }
+
+        private class TestControl : Control
+        {
+            public bool Rendered { get; private set; }
+
+            public override void Render(DrawingContext context)
+                => Rendered = true;
+        }
     }
 }