Browse Source

Merge branch 'master' into fix-win32-right-modifiers

pull/3320/head
Dariusz Komosiński 6 years ago
committed by GitHub
parent
commit
7162fd2243
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      native/Avalonia.Native/src/OSX/window.mm
  2. 2
      readme.md
  3. 6
      samples/ControlCatalog/MainView.xaml
  4. 85
      samples/ControlCatalog/Pages/ImagePage.xaml
  5. 39
      samples/ControlCatalog/Pages/ImagePage.xaml.cs
  6. 2
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  7. 64
      scripts/avalonia-rename.ps1
  8. 2
      src/Avalonia.Animation/IterationCount.cs
  9. 4
      src/Avalonia.Controls/DrawingPresenter.cs
  10. 45
      src/Avalonia.Controls/Image.cs
  11. 5
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  12. 5
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  13. 7
      src/Avalonia.Controls/Primitives/OverlayLayer.cs
  14. 38
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  15. 126
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  16. 2
      src/Avalonia.Controls/Remote/RemoteWidget.cs
  17. 34
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  18. 77
      src/Avalonia.Controls/Repeater/ViewManager.cs
  19. 6
      src/Avalonia.Controls/TextBox.cs
  20. 14
      src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs
  21. 32
      src/Avalonia.Layout/FlowLayoutAlgorithm.cs
  22. 16
      src/Avalonia.Layout/NonVirtualizingLayout.cs
  23. 14
      src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
  24. 2
      src/Avalonia.Layout/StackLayout.cs
  25. 54
      src/Avalonia.Layout/UniformGridLayout.cs
  26. 34
      src/Avalonia.Layout/UniformGridLayoutState.cs
  27. 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  28. 14
      src/Avalonia.Styling/Controls/ISetResourceParent.cs
  29. 62
      src/Avalonia.Styling/Controls/ResourceDictionary.cs
  30. 4
      src/Avalonia.Styling/StyledElement.cs
  31. 12
      src/Avalonia.Styling/Styling/Style.cs
  32. 20
      src/Avalonia.Styling/Styling/Styles.cs
  33. 8
      src/Avalonia.Visuals/Media/Color.cs
  34. 6
      src/Avalonia.Visuals/Media/Drawing.cs
  35. 21
      src/Avalonia.Visuals/Media/DrawingContext.cs
  36. 3
      src/Avalonia.Visuals/Media/DrawingGroup.cs
  37. 81
      src/Avalonia.Visuals/Media/DrawingImage.cs
  38. 5
      src/Avalonia.Visuals/Media/GeometryDrawing.cs
  39. 29
      src/Avalonia.Visuals/Media/IImage.cs
  40. 20
      src/Avalonia.Visuals/Media/Imaging/Bitmap.cs
  41. 11
      src/Avalonia.Visuals/Media/Imaging/IBitmap.cs
  42. 90
      src/Avalonia.Visuals/Media/MediaExtensions.cs
  43. 25
      src/Avalonia.Visuals/Media/StretchDirection.cs
  44. 4
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  45. 6
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  46. 13
      src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs
  47. 4
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  48. 4
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  49. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
  50. 18
      src/Avalonia.X11/Glx/Glx.cs
  51. 2
      src/Avalonia.X11/Glx/GlxDisplay.cs
  52. 2
      src/Avalonia.X11/X11IconLoader.cs
  53. 2
      src/Avalonia.X11/X11Platform.cs
  54. 4
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  55. 26
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs
  56. 10
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  57. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  58. 8
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  59. 5
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  60. 4
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  61. 2
      src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs
  62. 3
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs
  63. 2
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
  64. 14
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
  65. 70
      tests/Avalonia.Controls.UnitTests/ImageTests.cs
  66. 17
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  67. 54
      tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs
  68. 24
      tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
  69. 24
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs
  70. 123
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs
  71. 2
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  72. 18
      tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
  73. 45
      tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs
  74. 2
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  75. 136
      tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs

3
native/Avalonia.Native/src/OSX/window.mm

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

2
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). 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: Use these commands in the Package Manager console to install Avalonia manually:
``` ```

6
samples/ControlCatalog/MainView.xaml

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

85
samples/ControlCatalog/Pages/ImagePage.xaml

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

39
samples/ControlCatalog/Pages/ImagePage.xaml.cs

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

2
samples/RenderDemo/Pages/RenderTargetBitmapPage.cs

@ -39,7 +39,7 @@ namespace RenderDemo.Pages
ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100)); 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),
new Rect(0, 0, 200, 200)); new Rect(0, 0, 200, 200));
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);

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 .

2
src/Avalonia.Animation/IterationCount.cs

@ -63,7 +63,7 @@ namespace Avalonia.Animation
public IterationType RepeatType => _type; public IterationType RepeatType => _type;
/// <summary> /// <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> /// </summary>
public bool IsInfinite => _type == IterationType.Infinite; public bool IsInfinite => _type == IterationType.Infinite;

4
src/Avalonia.Controls/DrawingPresenter.cs

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

45
src/Avalonia.Controls/Image.cs

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

5
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@ -7,6 +7,7 @@ using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Rendering;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls.Notifications namespace Avalonia.Controls.Notifications
@ -14,7 +15,7 @@ namespace Avalonia.Controls.Notifications
/// <summary> /// <summary>
/// An <see cref="INotificationManager"/> that displays notifications in a <see cref="Window"/>. /// An <see cref="INotificationManager"/> that displays notifications in a <see cref="Window"/>.
/// </summary> /// </summary>
public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest
{ {
private IList _items; private IList _items;
@ -153,5 +154,7 @@ namespace Avalonia.Controls.Notifications
adornerLayer?.Children.Add(this); adornerLayer?.Children.Add(this);
} }
public bool HitTest(Point point) => VisualChildren.HitTestCustom(point);
} }
} }

5
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -138,10 +138,7 @@ namespace Avalonia.Controls.Primitives
} }
} }
public bool HitTest(Point point) public bool HitTest(Point point) => Children.HitTestCustom(point);
{
return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true);
}
private class AdornedElementInfo private class AdornedElementInfo
{ {

7
src/Avalonia.Controls/Primitives/OverlayLayer.cs

@ -21,11 +21,8 @@ namespace Avalonia.Controls.Primitives
return null; return null;
} }
public bool HitTest(Point point) public bool HitTest(Point point) => Children.HitTestCustom(point);
{
return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true);
}
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {

38
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -5,6 +5,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
@ -240,17 +241,14 @@ namespace Avalonia.Controls.Primitives
public override void BeginInit() public override void BeginInit()
{ {
base.BeginInit(); base.BeginInit();
++_updateCount;
_updateSelectedIndex = int.MinValue; InternalBeginInit();
} }
/// <inheritdoc/> /// <inheritdoc/>
public override void EndInit() public override void EndInit()
{ {
if (--_updateCount == 0) InternalEndInit();
{
UpdateFinished();
}
base.EndInit(); base.EndInit();
} }
@ -437,7 +435,8 @@ namespace Avalonia.Controls.Primitives
protected override void OnDataContextBeginUpdate() protected override void OnDataContextBeginUpdate()
{ {
base.OnDataContextBeginUpdate(); base.OnDataContextBeginUpdate();
++_updateCount;
InternalBeginInit();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -445,10 +444,7 @@ namespace Avalonia.Controls.Primitives
{ {
base.OnDataContextEndUpdate(); base.OnDataContextEndUpdate();
if (--_updateCount == 0) InternalEndInit();
{
UpdateFinished();
}
} }
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)
@ -1118,6 +1114,26 @@ namespace Avalonia.Controls.Primitives
} }
} }
private void InternalBeginInit()
{
if (_updateCount == 0)
{
_updateSelectedIndex = int.MinValue;
}
++_updateCount;
}
private void InternalEndInit()
{
Debug.Assert(_updateCount > 0);
if (--_updateCount == 0)
{
UpdateFinished();
}
}
private class Selection : IEnumerable<int> private class Selection : IEnumerable<int>
{ {
private readonly List<int> _list = new List<int>(); private readonly List<int> _list = new List<int>();

126
src/Avalonia.Controls/Primitives/ToggleButton.cs

@ -7,8 +7,14 @@ using Avalonia.Data;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
/// <summary>
/// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states.
/// </summary>
public class ToggleButton : Button public class ToggleButton : Button
{ {
/// <summary>
/// Defines the <see cref="IsChecked"/> property.
/// </summary>
public static readonly DirectProperty<ToggleButton, bool?> IsCheckedProperty = public static readonly DirectProperty<ToggleButton, bool?> IsCheckedProperty =
AvaloniaProperty.RegisterDirect<ToggleButton, bool?>( AvaloniaProperty.RegisterDirect<ToggleButton, bool?>(
nameof(IsChecked), nameof(IsChecked),
@ -17,9 +23,30 @@ namespace Avalonia.Controls.Primitives
unsetValue: null, unsetValue: null,
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="IsThreeState"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsThreeStateProperty = public static readonly StyledProperty<bool> IsThreeStateProperty =
AvaloniaProperty.Register<ToggleButton, bool>(nameof(IsThreeState)); AvaloniaProperty.Register<ToggleButton, bool>(nameof(IsThreeState));
/// <summary>
/// Defines the <see cref="Checked"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> CheckedEvent =
RoutedEvent.Register<ToggleButton, RoutedEventArgs>(nameof(Checked), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="Unchecked"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> UncheckedEvent =
RoutedEvent.Register<ToggleButton, RoutedEventArgs>(nameof(Unchecked), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="Unchecked"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> IndeterminateEvent =
RoutedEvent.Register<ToggleButton, RoutedEventArgs>(nameof(Indeterminate), RoutingStrategies.Bubble);
private bool? _isChecked = false; private bool? _isChecked = false;
static ToggleButton() static ToggleButton()
@ -27,14 +54,49 @@ namespace Avalonia.Controls.Primitives
PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == true, ":checked"); PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == true, ":checked");
PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == false, ":unchecked"); PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == false, ":unchecked");
PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == null, ":indeterminate"); PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == null, ":indeterminate");
IsCheckedProperty.Changed.AddClassHandler<ToggleButton>((x, e) => x.OnIsCheckedChanged(e));
}
/// <summary>
/// Raised when a <see cref="ToggleButton"/> is checked.
/// </summary>
public event EventHandler<RoutedEventArgs> Checked
{
add => AddHandler(CheckedEvent, value);
remove => RemoveHandler(CheckedEvent, value);
}
/// <summary>
/// Raised when a <see cref="ToggleButton"/> is unchecked.
/// </summary>
public event EventHandler<RoutedEventArgs> Unchecked
{
add => AddHandler(UncheckedEvent, value);
remove => RemoveHandler(UncheckedEvent, value);
}
/// <summary>
/// Raised when a <see cref="ToggleButton"/> is neither checked nor unchecked.
/// </summary>
public event EventHandler<RoutedEventArgs> Indeterminate
{
add => AddHandler(IndeterminateEvent, value);
remove => RemoveHandler(IndeterminateEvent, value);
} }
/// <summary>
/// Gets or sets whether the <see cref="ToggleButton"/> is checked.
/// </summary>
public bool? IsChecked public bool? IsChecked
{ {
get { return _isChecked; } get => _isChecked;
set { SetAndRaise(IsCheckedProperty, ref _isChecked, value); } set => SetAndRaise(IsCheckedProperty, ref _isChecked, value);
} }
/// <summary>
/// Gets or sets a value that indicates whether the control supports three states.
/// </summary>
public bool IsThreeState public bool IsThreeState
{ {
get => GetValue(IsThreeStateProperty); get => GetValue(IsThreeStateProperty);
@ -47,18 +109,78 @@ namespace Avalonia.Controls.Primitives
base.OnClick(); base.OnClick();
} }
/// <summary>
/// Toggles the <see cref="IsChecked"/> property.
/// </summary>
protected virtual void Toggle() protected virtual void Toggle()
{ {
if (IsChecked.HasValue) if (IsChecked.HasValue)
{
if (IsChecked.Value) if (IsChecked.Value)
{
if (IsThreeState) if (IsThreeState)
{
IsChecked = null; IsChecked = null;
}
else else
{
IsChecked = false; IsChecked = false;
}
}
else else
{
IsChecked = true; IsChecked = true;
}
}
else else
{
IsChecked = false; IsChecked = false;
}
}
/// <summary>
/// Called when <see cref="IsChecked"/> becomes true.
/// </summary>
/// <param name="e">Event arguments for the routed event that is raised by the default implementation of this method.</param>
protected virtual void OnChecked(RoutedEventArgs e)
{
RaiseEvent(e);
}
/// <summary>
/// Called when <see cref="IsChecked"/> becomes false.
/// </summary>
/// <param name="e">Event arguments for the routed event that is raised by the default implementation of this method.</param>
protected virtual void OnUnchecked(RoutedEventArgs e)
{
RaiseEvent(e);
}
/// <summary>
/// Called when <see cref="IsChecked"/> becomes null.
/// </summary>
/// <param name="e">Event arguments for the routed event that is raised by the default implementation of this method.</param>
protected virtual void OnIndeterminate(RoutedEventArgs e)
{
RaiseEvent(e);
}
private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e)
{
var newValue = (bool?)e.NewValue;
switch (newValue)
{
case true:
OnChecked(new RoutedEventArgs(CheckedEvent));
break;
case false:
OnUnchecked(new RoutedEventArgs(UncheckedEvent));
break;
default:
OnIndeterminate(new RoutedEventArgs(IndeterminateEvent));
break;
}
} }
} }
} }

2
src/Avalonia.Controls/Remote/RemoteWidget.cs

@ -83,7 +83,7 @@ namespace Avalonia.Controls.Remote
Marshal.Copy(_lastFrame.Data, y * _lastFrame.Stride, Marshal.Copy(_lastFrame.Data, y * _lastFrame.Stride,
new IntPtr(l.Address.ToInt64() + l.RowBytes * y), lineLen); 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)); new Rect(Bounds.Size));
} }
base.Render(context); base.Render(context);

34
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -562,33 +562,35 @@ namespace Avalonia.Controls
if (Layout != null) if (Layout != null)
{ {
if (Layout is VirtualizingLayout virtualLayout) var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
try
{
_processingItemsSourceChange = args; _processingItemsSourceChange = args;
try if (Layout is VirtualizingLayout virtualLayout)
{ {
virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
} }
finally else if (Layout is NonVirtualizingLayout nonVirtualLayout)
{
_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)
{ {
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(); InvalidateMeasure();
} }

77
src/Avalonia.Controls/Repeater/ViewManager.cs

@ -109,11 +109,22 @@ namespace Avalonia.Controls
public void ClearElementToElementFactory(IControl element) public void ClearElementToElementFactory(IControl element)
{ {
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var clearedIndex = virtInfo.Index;
_owner.OnElementClearing(element); _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(); virtInfo.MoveOwnershipToElementFactory();
if (_lastFocusedElement == element) if (_lastFocusedElement == element)
@ -121,9 +132,8 @@ namespace Avalonia.Controls
// Focused element is going away. Remove the tracked last focused element // 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 // and pick a reasonable next focus if we can find one within the layout
// realized elements. // realized elements.
MoveFocusFromClearedIndex(clearedIndex); MoveFocusFromClearedIndex(virtInfo.Index);
} }
} }
private void MoveFocusFromClearedIndex(int clearedIndex) private void MoveFocusFromClearedIndex(int clearedIndex)
@ -190,7 +200,8 @@ namespace Avalonia.Controls
{ {
if (virtInfo == null) 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; return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1;
@ -515,21 +526,52 @@ namespace Avalonia.Controls
return element; 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) private IControl GetElementFromElementFactory(int index)
{ {
// The view generator is the provider of last resort. // 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; return providedElementFactory;
if (itemTemplateFactory == null) }
IControl GetElement()
{ {
// If no ItemTemplate was provided, use a default if (providedElementFactory == null)
var factory = FuncDataTemplate.Default; {
_owner.ItemTemplate = factory; if (data is IControl dataAsElement)
itemTemplateFactory = _owner.ItemTemplateShim; {
return dataAsElement;
}
}
var elementFactory = GetElementFactory();
return elementFactory.GetElement(_owner, data);
} }
var data = _owner.ItemsSourceView.GetAt(index); var element = GetElement();
var element = itemTemplateFactory.GetElement(_owner, data);
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
if (virtInfo == null) if (virtInfo == null)
@ -537,8 +579,11 @@ namespace Avalonia.Controls
virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
} }
// Prepare the element if (data != element)
element.DataContext = data; {
// Prepare the element
element.DataContext = data;
}
virtInfo.MoveOwnershipToLayoutFromElementFactory( virtInfo.MoveOwnershipToLayoutFromElementFactory(
index, index,

6
src/Avalonia.Controls/TextBox.cs

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

14
src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs

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

32
src/Avalonia.Layout/FlowLayoutAlgorithm.cs

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

16
src/Avalonia.Layout/NonVirtualizingLayout.cs

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

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

54
src/Avalonia.Layout/UniformGridLayout.cs

@ -110,6 +110,12 @@ namespace Avalonia.Layout
public static readonly StyledProperty<double> MinRowSpacingProperty = public static readonly StyledProperty<double> MinRowSpacingProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinRowSpacing)); 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> /// <summary>
/// Defines the <see cref="Orientation"/> property. /// Defines the <see cref="Orientation"/> property.
/// </summary> /// </summary>
@ -123,6 +129,7 @@ namespace Avalonia.Layout
private double _minColumnSpacing; private double _minColumnSpacing;
private UniformGridLayoutItemsJustification _itemsJustification; private UniformGridLayoutItemsJustification _itemsJustification;
private UniformGridLayoutItemsStretch _itemsStretch; private UniformGridLayoutItemsStretch _itemsStretch;
private int _maximumRowsOrColumns = int.MaxValue;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UniformGridLayout"/> class. /// Initializes a new instance of the <see cref="UniformGridLayout"/> class.
@ -219,6 +226,15 @@ namespace Avalonia.Layout
set => SetValue(MinRowSpacingProperty, value); 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> /// <summary>
/// Gets or sets the axis along which items are laid out. /// Gets or sets the axis along which items are laid out.
/// </summary> /// </summary>
@ -269,15 +285,17 @@ namespace Avalonia.Layout
{ {
var gridState = (UniformGridLayoutState)context.LayoutState; var gridState = (UniformGridLayoutState)context.LayoutState;
var lastExtent = gridState.FlowAlgorithm.LastExtent; var lastExtent = gridState.FlowAlgorithm.LastExtent;
int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); var itemsPerLine = Math.Min( // note use of unsigned ints
double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))),
double realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); 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) if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize)
{ {
double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent)); double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent));
int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context)); 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); bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context);
} }
} }
@ -299,7 +317,9 @@ namespace Avalonia.Layout
int count = context.ItemCount; int count = context.ItemCount;
if (targetIndex >= 0 && targetIndex < count) 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; int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine;
index = indexOfFirstInLine; index = indexOfFirstInLine;
var state = context.LayoutState as UniformGridLayoutState; var state = context.LayoutState as UniformGridLayoutState;
@ -329,17 +349,21 @@ namespace Avalonia.Layout
// Constants // Constants
int itemsCount = context.ItemCount; int itemsCount = context.ItemCount;
double availableSizeMinor = _orientation.Minor(availableSize); double availableSizeMinor = _orientation.Minor(availableSize);
int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ? int itemsPerLine =
(int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount); (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); double lineSize = GetMajorSizeWithSpacing(context);
if (itemsCount > 0) if (itemsCount > 0)
{ {
_orientation.SetMinorSize( _orientation.SetMinorSize(
ref extent, ref extent,
!double.IsInfinity(availableSizeMinor) ? !double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ?
availableSizeMinor : availableSizeMinor :
Math.Max(0.0, itemsCount * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing));
_orientation.SetMajorSize( _orientation.SetMajorSize(
ref extent, ref extent,
Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing)); Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing));
@ -398,7 +422,7 @@ namespace Avalonia.Layout
// Set the width and height on the grid state. If the user already set them then use the preset. // 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. // 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; 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( var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize, availableSize,
@ -406,6 +430,7 @@ namespace Avalonia.Layout
true, true,
MinItemSpacing, MinItemSpacing,
LineSpacing, LineSpacing,
_maximumRowsOrColumns,
_orientation.ScrollOrientation, _orientation.ScrollOrientation,
LayoutId); LayoutId);
@ -421,6 +446,7 @@ namespace Avalonia.Layout
var value = GetFlowAlgorithm(context).Arrange( var value = GetFlowAlgorithm(context).Arrange(
finalSize, finalSize,
context, context,
true,
(FlowLayoutAlgorithm.LineAlignment)_itemsJustification, (FlowLayoutAlgorithm.LineAlignment)_itemsJustification,
LayoutId); LayoutId);
return new Size(value.Width, value.Height); return new Size(value.Width, value.Height);
@ -471,6 +497,10 @@ namespace Avalonia.Layout
{ {
_minItemHeight = (double)args.NewValue; _minItemHeight = (double)args.NewValue;
} }
else if (args.Property == MaximumRowsOrColumnsProperty)
{
_maximumRowsOrColumns = (int)args.NewValue;
}
InvalidateLayout(); InvalidateLayout();
} }
@ -499,7 +529,9 @@ namespace Avalonia.Layout
Rect lastExtent, Rect lastExtent,
VirtualizingLayoutContext context) 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 rowIndex = (int)(index / itemsPerLine);
int indexInRow = index - (rowIndex * itemsPerLine); int indexInRow = index - (rowIndex * itemsPerLine);

34
src/Avalonia.Layout/UniformGridLayoutState.cs

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

2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -101,7 +101,7 @@ namespace Avalonia.Native
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60)) .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<ISystemDialogImpl>().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind<ISystemDialogImpl>().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs()))
.Bind<IWindowingPlatformGlFeature>().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature())) .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()); .Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider());
} }

14
src/Avalonia.Styling/Styling/ISetStyleParent.cs → src/Avalonia.Styling/Controls/ISetResourceParent.cs

@ -1,29 +1,27 @@
using Avalonia.Controls; namespace Avalonia.Controls
namespace Avalonia.Styling
{ {
/// <summary> /// <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> /// </summary>
/// <remarks> /// <remarks>
/// You should not usually need to use this interface - it is for internal use only. /// You should not usually need to use this interface - it is for internal use only.
/// </remarks> /// </remarks>
public interface ISetStyleParent : IStyle public interface ISetResourceParent : IResourceNode
{ {
/// <summary> /// <summary>
/// Sets the style parent. /// Sets the resource parent.
/// </summary> /// </summary>
/// <param name="parent">The parent.</param> /// <param name="parent">The parent.</param>
void SetParent(IResourceNode parent); void SetParent(IResourceNode parent);
/// <summary> /// <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> /// </summary>
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
/// <remarks> /// <remarks>
/// This method will be called automatically by the framework, you should not need to call /// This method will be called automatically by the framework, you should not need to call
/// this method yourself. /// this method yourself.
/// </remarks> /// </remarks>
void NotifyResourcesChanged(ResourcesChangedEventArgs e); void ParentResourcesChanged(ResourcesChangedEventArgs e);
} }
} }

62
src/Avalonia.Styling/Controls/ResourceDictionary.cs

@ -12,8 +12,12 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// An indexed dictionary of resources. /// An indexed dictionary of resources.
/// </summary> /// </summary>
public class ResourceDictionary : AvaloniaDictionary<object, object>, IResourceDictionary public class ResourceDictionary : AvaloniaDictionary<object, object>,
IResourceDictionary,
IResourceNode,
ISetResourceParent
{ {
private IResourceNode _parent;
private AvaloniaList<IResourceProvider> _mergedDictionaries; private AvaloniaList<IResourceProvider> _mergedDictionaries;
/// <summary> /// <summary>
@ -39,6 +43,12 @@ namespace Avalonia.Controls
_mergedDictionaries.ForEachItem( _mergedDictionaries.ForEachItem(
x => x =>
{ {
if (x is ISetResourceParent setParent)
{
setParent.SetParent(this);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
}
if (x.HasResources) if (x.HasResources)
{ {
OnResourcesChanged(); OnResourcesChanged();
@ -48,11 +58,18 @@ namespace Avalonia.Controls
}, },
x => x =>
{ {
if (x is ISetResourceParent setParent)
{
setParent.SetParent(null);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
}
if (x.HasResources) if (x.HasResources)
{ {
OnResourcesChanged(); OnResourcesChanged();
} }
(x as ISetResourceParent)?.SetParent(null);
x.ResourcesChanged -= MergedDictionaryResourcesChanged; x.ResourcesChanged -= MergedDictionaryResourcesChanged;
}, },
() => { }); () => { });
@ -68,6 +85,27 @@ namespace Avalonia.Controls
get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false); 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/> /// <inheritdoc/>
public bool TryGetResource(object key, out object value) public bool TryGetResource(object key, out object value)
{ {
@ -95,7 +133,27 @@ namespace Avalonia.Controls
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); 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(); private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged();
} }
} }

4
src/Avalonia.Styling/StyledElement.cs

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

12
src/Avalonia.Styling/Styling/Style.cs

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

20
src/Avalonia.Styling/Styling/Styles.cs

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

8
src/Avalonia.Visuals/Media/Color.cs

@ -19,22 +19,22 @@ namespace Avalonia.Media
} }
/// <summary> /// <summary>
/// Gets or sets the Alpha component of the color. /// Gets the Alpha component of the color.
/// </summary> /// </summary>
public byte A { get; } public byte A { get; }
/// <summary> /// <summary>
/// Gets or sets the Red component of the color. /// Gets the Red component of the color.
/// </summary> /// </summary>
public byte R { get; } public byte R { get; }
/// <summary> /// <summary>
/// Gets or sets the Green component of the color. /// Gets the Green component of the color.
/// </summary> /// </summary>
public byte G { get; } public byte G { get; }
/// <summary> /// <summary>
/// Gets or sets the Blue component of the color. /// Gets the Blue component of the color.
/// </summary> /// </summary>
public byte B { get; } public byte B { get; }

6
src/Avalonia.Visuals/Media/Drawing.cs

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

21
src/Avalonia.Visuals/Media/DrawingContext.cs

@ -74,18 +74,29 @@ namespace Avalonia.Media
public Matrix CurrentContainerTransform => _currentContainerTransform; public Matrix CurrentContainerTransform => _currentContainerTransform;
/// <summary> /// <summary>
/// Draws a bitmap image. /// Draws an image.
/// </summary> /// </summary>
/// <param name="source">The bitmap image.</param> /// <param name="source">The image.</param>
/// <param name="opacity">The opacity to draw with.</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="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param> /// <param name="destRect">The rect in the output to draw to.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</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); Contract.Requires<ArgumentNullException>(source != null);
PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect, bitmapInterpolationMode); source.Draw(this, sourceRect, destRect, bitmapInterpolationMode);
} }
/// <summary> /// <summary>

3
src/Avalonia.Visuals/Media/DrawingGroup.cs

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

81
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);
}
}

5
src/Avalonia.Visuals/Media/GeometryDrawing.cs

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

29
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
src/Avalonia.Visuals/Media/Imaging/Bitmap.cs

@ -5,6 +5,7 @@ using System;
using System.IO; using System.IO;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Media.Imaging namespace Avalonia.Media.Imaging
{ {
@ -94,9 +95,28 @@ namespace Avalonia.Media.Imaging
PlatformImpl.Item.Save(fileName); PlatformImpl.Item.Save(fileName);
} }
/// <summary>
/// Saves the bitmap to a stream.
/// </summary>
/// <param name="stream">The stream.</param>
public void Save(Stream stream) public void Save(Stream stream)
{ {
PlatformImpl.Item.Save(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);
}
} }
} }

11
src/Avalonia.Visuals/Media/Imaging/IBitmap.cs

@ -11,7 +11,7 @@ namespace Avalonia.Media.Imaging
/// <summary> /// <summary>
/// Represents a bitmap image. /// Represents a bitmap image.
/// </summary> /// </summary>
public interface IBitmap : IDisposable public interface IBitmap : IImage, IDisposable
{ {
/// <summary> /// <summary>
/// Gets the dots per inch (DPI) of the image. /// Gets the dots per inch (DPI) of the image.
@ -32,15 +32,6 @@ namespace Avalonia.Media.Imaging
/// </summary> /// </summary>
IRef<IBitmapImpl> PlatformImpl { get; } 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> /// <summary>
/// Saves the bitmap to a file. /// Saves the bitmap to a file.
/// </summary> /// </summary>

90
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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -16,24 +17,82 @@ namespace Avalonia.Media
/// <param name="stretch">The stretch mode.</param> /// <param name="stretch">The stretch mode.</param>
/// <param name="destinationSize">The size of the destination viewport.</param> /// <param name="destinationSize">The size of the destination viewport.</param>
/// <param name="sourceSize">The size of the source.</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> /// <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; var scaleX = 1.0;
double scaleY = 1; 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; // Compute scaling factors for both axes
scaleY = destinationSize.Height / sourceSize.Height; 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: // If not preserving aspect ratio, then just apply transform to fit
scaleX = scaleY = Math.Min(scaleX, scaleY); 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; break;
case Stretch.UniformToFill:
scaleX = scaleY = Math.Max(scaleX, scaleY); case StretchDirection.Both:
break;
default:
break; break;
} }
} }
@ -47,10 +106,15 @@ namespace Avalonia.Media
/// <param name="stretch">The stretch mode.</param> /// <param name="stretch">The stretch mode.</param>
/// <param name="destinationSize">The size of the destination viewport.</param> /// <param name="destinationSize">The size of the destination viewport.</param>
/// <param name="sourceSize">The size of the source.</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> /// <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
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,
}
}

4
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="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param> /// <param name="destRect">The rect in the output to draw to.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</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> /// <summary>
/// Draws a bitmap image. /// Draws a bitmap image.
@ -42,7 +42,7 @@ namespace Avalonia.Platform
/// <param name="opacityMask">The opacity mask to draw with.</param> /// <param name="opacityMask">The opacity mask to draw with.</param>
/// <param name="opacityMaskRect">The destination rect for the opacity mask.</param> /// <param name="opacityMaskRect">The destination rect for the opacity mask.</param>
/// <param name="destRect">The rect in the output to draw to.</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> /// <summary>
/// Draws a line. /// Draws a line.

6
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

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

13
src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs

@ -1,3 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.VisualTree;
namespace Avalonia.Rendering namespace Avalonia.Rendering
{ {
/// <summary> /// <summary>
@ -9,4 +13,13 @@ namespace Avalonia.Rendering
{ {
bool HitTest(Point point); bool HitTest(Point point);
} }
public static class CustomSimpleHitTestExtensions
{
public static bool HitTestCustom(this IVisual visual, Point point)
=> (visual as ICustomSimpleHitTest)?.HitTest(point) ?? visual.TransformedBounds?.Contains(point) == true;
public static bool HitTestCustom(this IEnumerable<IVisual> children, Point point)
=> children.Any(ctrl => ctrl.HitTestCustom(point));
}
} }

4
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@ -307,7 +307,9 @@ namespace Avalonia.Rendering
if (!child.ClipToBounds || clipRect.Intersects(childBounds)) 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); Render(context, child, childClipRect);
} }
else else

4
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -115,7 +115,7 @@ namespace Avalonia.Rendering.SceneGraph
} }
/// <inheritdoc/> /// <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>(); var next = NextDrawAs<ImageNode>();
@ -130,7 +130,7 @@ namespace Avalonia.Rendering.SceneGraph
} }
/// <inheritdoc/> /// <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. // This method is currently only used to composite layers so shouldn't be called here.
throw new NotSupportedException(); throw new NotSupportedException();

2
src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs

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

18
src/Avalonia.X11/Glx/Glx.cs

@ -84,8 +84,24 @@ namespace Avalonia.X11.Glx
[GlEntryPoint("glGetError")] [GlEntryPoint("glGetError")]
public GlGetError GetError { get; } public GlGetError GetError { get; }
public GlxInterface() : base(GlxGetProcAddress) public GlxInterface() : base(SafeGetProcAddress)
{ {
} }
// Ignores egl functions.
// On some Linux systems, glXGetProcAddress will return valid pointers for even EGL functions.
// This makes Skia try to load some data from EGL,
// which can then cause segmentation faults because they return garbage.
public static IntPtr SafeGetProcAddress(string proc, bool optional)
{
if (proc.StartsWith("egl", StringComparison.InvariantCulture))
{
return IntPtr.Zero;
}
return GlxConverted(proc, optional);
}
private static readonly Func<string, bool, IntPtr> GlxConverted = ConvertNative(GlxGetProcAddress);
} }
} }

2
src/Avalonia.X11/Glx/GlxDisplay.cs

@ -87,7 +87,7 @@ namespace Avalonia.X11.Glx
ImmediateContext.MakeCurrent(); ImmediateContext.MakeCurrent();
var err = Glx.GetError(); var err = Glx.GetError();
GlInterface = new GlInterface(GlxInterface.GlxGetProcAddress); GlInterface = new GlInterface(GlxInterface.SafeGetProcAddress);
if (GlInterface.Version == null) if (GlInterface.Version == null)
throw new OpenGlException("GL version string is null, aborting"); throw new OpenGlException("GL version string is null, aborting");
if (GlInterface.Renderer == null) if (GlInterface.Renderer == null)

2
src/Avalonia.X11/X11IconLoader.cs

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

2
src/Avalonia.X11/X11Platform.cs

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

4
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -87,7 +87,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public string StringFormat { get; set; } public string StringFormat { get; set; }
public RelativeSource RelativeSource { get; set; } public RelativeSource RelativeSource { get; set; }
public object TargetNullValue { get; set; } public object TargetNullValue { get; set; } = AvaloniaProperty.UnsetValue;
} }
} }

26
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs

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

10
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

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

2
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) void Add(string type, string conv)
=> AddType(typeSystem.GetType(type), typeSystem.GetType(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"); var ilist = typeSystem.GetType("System.Collections.Generic.IList`1");
AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")),
typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter"));

8
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -110,7 +110,7 @@ namespace Avalonia.Skia
} }
/// <inheritdoc /> /// <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 drawableImage = (IDrawableBitmapImpl)source.Item;
var s = sourceRect.ToSKRect(); var s = sourceRect.ToSKRect();
@ -146,10 +146,10 @@ namespace Avalonia.Skia
} }
/// <inheritdoc /> /// <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); 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(); PopOpacityMask();
} }
@ -437,7 +437,7 @@ namespace Avalonia.Skia
context.Clear(Colors.Transparent); context.Clear(Colors.Transparent);
context.PushClip(calc.IntermediateClip); context.PushClip(calc.IntermediateClip);
context.Transform = calc.IntermediateTransform; context.Transform = calc.IntermediateTransform;
context.DrawImage( context.DrawBitmap(
RefCountable.CreateUnownedNotClonable(tileBrushImage), RefCountable.CreateUnownedNotClonable(tileBrushImage),
1, 1,
sourceRect, sourceRect,

5
src/Skia/Avalonia.Skia/FontManagerImpl.cs

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

4
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="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param> /// <param name="destRect">The rect in the output to draw to.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</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)) 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="opacityMask">The opacity mask to draw with.</param>
/// <param name="opacityMaskRect">The destination rect for the opacity mask.</param> /// <param name="opacityMaskRect">The destination rect for the opacity mask.</param>
/// <param name="destRect">The rect in the output to draw to.</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 d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext))
using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value)) using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value))

2
src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs

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

3
src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs

@ -30,7 +30,7 @@ namespace Avalonia.Direct2D1.Media
_direct2DBitmap = d2DBitmap ?? throw new ArgumentNullException(nameof(d2DBitmap)); _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 PixelSize PixelSize => _direct2DBitmap.PixelSize.ToAvalonia();
public override void Dispose() public override void Dispose()
@ -58,3 +58,4 @@ namespace Avalonia.Direct2D1.Media
} }
} }
} }
;

2
src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs

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

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

70
tests/Avalonia.Controls.UnitTests/ImageTests.cs

@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Measure_Should_Return_Correct_Size_For_No_Stretch() 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(); var target = new Image();
target.Stretch = Stretch.None; target.Stretch = Stretch.None;
target.Source = bitmap; target.Source = bitmap;
@ -26,7 +26,7 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Measure_Should_Return_Correct_Size_For_Fill_Stretch() 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(); var target = new Image();
target.Stretch = Stretch.Fill; target.Stretch = Stretch.Fill;
target.Source = bitmap; target.Source = bitmap;
@ -39,7 +39,7 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Measure_Should_Return_Correct_Size_For_Uniform_Stretch() 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(); var target = new Image();
target.Stretch = Stretch.Uniform; target.Stretch = Stretch.Uniform;
target.Source = bitmap; target.Source = bitmap;
@ -52,7 +52,7 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Measure_Should_Return_Correct_Size_For_UniformToFill_Stretch() 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(); var target = new Image();
target.Stretch = Stretch.UniformToFill; target.Stretch = Stretch.UniformToFill;
target.Source = bitmap; target.Source = bitmap;
@ -62,10 +62,59 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Size(50, 50), target.DesiredSize); 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] [Fact]
public void Arrange_Should_Return_Correct_Size_For_No_Stretch() 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(); var target = new Image();
target.Stretch = Stretch.None; target.Stretch = Stretch.None;
target.Source = bitmap; target.Source = bitmap;
@ -79,7 +128,7 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Arrange_Should_Return_Correct_Size_For_Fill_Stretch() 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(); var target = new Image();
target.Stretch = Stretch.Fill; target.Stretch = Stretch.Fill;
target.Source = bitmap; target.Source = bitmap;
@ -93,7 +142,7 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Arrange_Should_Return_Correct_Size_For_Uniform_Stretch() 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(); var target = new Image();
target.Stretch = Stretch.Uniform; target.Stretch = Stretch.Uniform;
target.Source = bitmap; target.Source = bitmap;
@ -107,7 +156,7 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Arrange_Should_Return_Correct_Size_For_UniformToFill_Stretch() 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(); var target = new Image();
target.Stretch = Stretch.UniformToFill; target.Stretch = Stretch.UniformToFill;
target.Source = bitmap; target.Source = bitmap;
@ -117,5 +166,10 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Size(25, 100), target.Bounds.Size); 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));
}
} }
} }

17
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -129,6 +129,23 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(-1, target.SelectedIndex); Assert.Equal(-1, target.SelectedIndex);
} }
[Fact]
public void SelectedIndex_Should_Be_Minus_1_Without_Initialize()
{
var items = new[]
{
new Item(),
new Item(),
};
var target = new ListBox();
target.Items = items;
target.Template = Template();
target.DataContext = new object();
Assert.Equal(-1, target.SelectedIndex);
}
[Fact] [Fact]
public void SelectedIndex_Should_Be_0_After_Initialize_With_AlwaysSelected() public void SelectedIndex_Should_Be_0_After_Initialize_With_AlwaysSelected()
{ {

54
tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs

@ -1,5 +1,4 @@
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -63,6 +62,54 @@ namespace Avalonia.Controls.Primitives.UnitTests
Assert.Null(threeStateButton.IsChecked); Assert.Null(threeStateButton.IsChecked);
} }
[Fact]
public void ToggleButton_Events_Are_Raised_On_Is_Checked_Changes()
{
var threeStateButton = new ToggleButton();
bool checkedRaised = false;
threeStateButton.Checked += (_, __) => checkedRaised = true;
threeStateButton.IsChecked = true;
Assert.True(checkedRaised);
bool uncheckedRaised = false;
threeStateButton.Unchecked += (_, __) => uncheckedRaised = true;
threeStateButton.IsChecked = false;
Assert.True(uncheckedRaised);
bool indeterminateRaised = false;
threeStateButton.Indeterminate += (_, __) => indeterminateRaised = true;
threeStateButton.IsChecked = null;
Assert.True(indeterminateRaised);
}
[Fact]
public void ToggleButton_Events_Are_Raised_When_Toggling()
{
var threeStateButton = new TestToggleButton { IsThreeState = true };
bool checkedRaised = false;
threeStateButton.Checked += (_, __) => checkedRaised = true;
threeStateButton.Toggle();
Assert.True(checkedRaised);
bool indeterminateRaised = false;
threeStateButton.Indeterminate += (_, __) => indeterminateRaised = true;
threeStateButton.Toggle();
Assert.True(indeterminateRaised);
bool uncheckedRaised = false;
threeStateButton.Unchecked += (_, __) => uncheckedRaised = true;
threeStateButton.Toggle();
Assert.True(uncheckedRaised);
}
private class Class1 : NotifyingBase private class Class1 : NotifyingBase
{ {
private bool _foo; private bool _foo;
@ -80,5 +127,10 @@ namespace Avalonia.Controls.Primitives.UnitTests
set { nullableFoo = value; RaisePropertyChanged(); } set { nullableFoo = value; RaisePropertyChanged(); }
} }
} }
private class TestToggleButton : ToggleButton
{
public new void Toggle() => base.Toggle();
}
} }
} }

24
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] [Fact]
public void Should_Create_Typeface_For_Unknown_Font() public void Should_Create_Typeface_For_Unknown_Font()
{ {

24
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs

@ -38,7 +38,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal("foobar", textBlock.Text); Assert.Equal("foobar", textBlock.Text);
} }
} }
[Fact] [Fact]
public void BindingExtension_Binds_To_TargetNullValue() public void BindingExtension_Binds_To_TargetNullValue()
{ {
@ -65,6 +65,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
} }
} }
[Fact]
public void BindingExtension_TargetNullValue_UnsetByDefault()
{
using (StyledWindow())
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock Name='textBlock' IsVisible='{Binding Foo, Converter={x:Static ObjectConverters.IsNotNull}}'/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
window.DataContext = new FooBar();
window.Show();
Assert.Equal(false, textBlock.IsVisible);
}
}
private class FooBar private class FooBar
{ {
public object Foo { get; } = null; public object Foo { get; } = null;

123
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)))
}
};
}
}
}

2
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)); ctx.DrawRectangle(Brushes.Pink, null, new Rect(0, 20, 100, 10));
var rc = new Rect(0, 0, 60, 60); 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")); rtb.Save(System.IO.Path.Combine(OutputPath, testName + ".out.png"));
} }

18
tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Reflection; using System.Reflection;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
@ -29,6 +30,23 @@ namespace Avalonia.Skia.UnitTests
Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant); 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] [Fact]
public void Should_Create_Typeface_For_Unknown_Font() public void Should_Create_Typeface_For_Unknown_Font()
{ {

45
tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs

@ -3,6 +3,7 @@
using System; using System;
using Avalonia.Controls; using Avalonia.Controls;
using Moq;
using Xunit; using Xunit;
namespace Avalonia.Styling.UnitTests namespace Avalonia.Styling.UnitTests
@ -136,7 +137,7 @@ namespace Avalonia.Styling.UnitTests
} }
[Fact] [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 var target = new ResourceDictionary
{ {
@ -145,31 +146,45 @@ namespace Avalonia.Styling.UnitTests
new ResourceDictionary(), new ResourceDictionary(),
} }
}; };
var raised = false; var raised = false;
target.ResourcesChanged += (_, __) => raised = true; target.ResourcesChanged += (_, __) => raised = true;
target.MergedDictionaries.RemoveAt(0); ((IResourceDictionary)target.MergedDictionaries[0]).Add("foo", "bar");
Assert.False(raised); Assert.True(raised);
} }
[Fact] [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 var target = new ResourceDictionary();
{ var merged = new Mock<ISetResourceParent>();
MergedDictionaries =
{
new ResourceDictionary(),
}
};
var raised = false; target.MergedDictionaries.Add(merged.Object);
merged.ResetCalls();
target.ResourcesChanged += (_, __) => raised = true; target.Add("foo", "bar");
((IResourceDictionary)target.MergedDictionaries[0]).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);
} }
} }
} }

2
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 context = Mock.Get(target.RenderTarget.CreateDrawingContext(null));
var borderLayer = target.Layers[border].Bitmap; 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] [Fact]

136
tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs

@ -178,5 +178,141 @@ namespace Avalonia.Visuals.UnitTests.Rendering
Assert.Equal(1, rendered); 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;
}
} }
} }

Loading…
Cancel
Save