Browse Source

Merge branch 'master' into ref-bitmaps

pull/1277/head
Steven Kirk 9 years ago
committed by GitHub
parent
commit
4cace359b5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 38
      src/Avalonia.Controls/Border.cs
  2. 127
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  3. 66
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  4. 16
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  5. 37
      src/Avalonia.Controls/TextBox.cs
  6. 14
      src/Avalonia.Controls/Window.cs
  7. 31
      src/Avalonia.Visuals/Media/EllipseGeometry.cs
  8. 125
      src/Avalonia.Visuals/Media/Geometry.cs
  9. 3
      src/Avalonia.Visuals/Media/GeometryDrawing.cs
  10. 66
      src/Avalonia.Visuals/Media/LineGeometry.cs
  11. 68
      src/Avalonia.Visuals/Media/PathGeometry.cs
  12. 73
      src/Avalonia.Visuals/Media/PolylineGeometry.cs
  13. 22
      src/Avalonia.Visuals/Media/RectangleGeometry.cs
  14. 20
      src/Avalonia.Visuals/Media/StreamGeometry.cs
  15. 16
      src/Avalonia.Visuals/Platform/IGeometryImpl.cs
  16. 24
      src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs
  17. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  18. 6
      src/Avalonia.Visuals/VisualTree/TransformedBounds.cs
  19. 2
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  20. 18
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  21. 56
      src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
  22. 59
      src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs
  23. 2
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  24. 14
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  25. 10
      src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs
  26. 15
      src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
  27. 194
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs
  28. 4
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs
  29. 155
      tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
  30. 3
      tests/Avalonia.Direct2D1.UnitTests/Media/GeometryTests.cs
  31. 110
      tests/Avalonia.Layout.UnitTests/LayoutableTests.cs
  32. 12
      tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
  33. 115
      tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs
  34. 39
      tests/Avalonia.Visuals.UnitTests/Media/RectangleGeometryTests.cs
  35. 19
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

38
src/Avalonia.Controls/Border.cs

@ -108,18 +108,7 @@ namespace Avalonia.Controls
/// <returns>The desired size of the control.</returns> /// <returns>The desired size of the control.</returns>
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
var child = Child; return MeasureOverrideImpl(availableSize, Child, Padding, BorderThickness);
var padding = Padding + new Thickness(BorderThickness);
if (child != null)
{
child.Measure(availableSize.Deflate(padding));
return child.DesiredSize.Inflate(padding);
}
else
{
return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
}
} }
/// <summary> /// <summary>
@ -129,15 +118,32 @@ namespace Avalonia.Controls
/// <returns>The space taken.</returns> /// <returns>The space taken.</returns>
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
var child = Child; if (Child != null)
if (child != null)
{ {
var padding = Padding + new Thickness(BorderThickness); var padding = Padding + new Thickness(BorderThickness);
child.Arrange(new Rect(finalSize).Deflate(padding)); Child.Arrange(new Rect(finalSize).Deflate(padding));
} }
return finalSize; return finalSize;
} }
internal static Size MeasureOverrideImpl(
Size availableSize,
IControl child,
Thickness padding,
double borderThickness)
{
padding += new Thickness(borderThickness);
if (child != null)
{
child.Measure(availableSize.Deflate(padding));
return child.DesiredSize.Inflate(padding);
}
else
{
return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
}
}
} }
} }

127
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -2,14 +2,12 @@
// 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 System.Reactive.Linq;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
@ -340,94 +338,121 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/> /// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
var child = Child; return Border.MeasureOverrideImpl(availableSize, Child, Padding, BorderThickness);
var padding = Padding + new Thickness(BorderThickness); }
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
return ArrangeOverrideImpl(finalSize, new Vector());
}
/// <summary>
/// Called when the <see cref="Content"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void ContentChanged(AvaloniaPropertyChangedEventArgs e)
{
_createdChild = false;
if (child != null) if (((ILogical)this).IsAttachedToLogicalTree)
{ {
child.Measure(availableSize.Deflate(padding)); UpdateChild();
return child.DesiredSize.Inflate(padding);
} }
else else if (Child != null)
{ {
return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top); VisualChildren.Remove(Child);
LogicalChildren.Remove(Child);
Child = null;
_dataTemplate = null;
} }
InvalidateMeasure();
} }
/// <inheritdoc/> internal Size ArrangeOverrideImpl(Size finalSize, Vector offset)
protected override Size ArrangeOverride(Size finalSize)
{ {
var child = Child; if (Child != null)
if (child != null)
{ {
var padding = Padding + new Thickness(BorderThickness); var padding = Padding;
var sizeMinusPadding = finalSize.Deflate(padding); var borderThickness = BorderThickness;
var size = sizeMinusPadding; var horizontalContentAlignment = HorizontalContentAlignment;
var horizontalAlignment = HorizontalContentAlignment; var verticalContentAlignment = VerticalContentAlignment;
var verticalAlignment = VerticalContentAlignment; var useLayoutRounding = UseLayoutRounding;
var originX = padding.Left; var availableSizeMinusMargins = new Size(
var originY = padding.Top; Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness),
Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness));
if (horizontalAlignment != HorizontalAlignment.Stretch) var size = availableSizeMinusMargins;
var scale = GetLayoutScale();
var originX = offset.X + padding.Left + borderThickness;
var originY = offset.Y + padding.Top + borderThickness;
if (horizontalContentAlignment != HorizontalAlignment.Stretch)
{ {
size = size.WithWidth(child.DesiredSize.Width); size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right));
} }
if (verticalAlignment != VerticalAlignment.Stretch) if (verticalContentAlignment != VerticalAlignment.Stretch)
{ {
size = size.WithHeight(child.DesiredSize.Height); size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom));
} }
switch (horizontalAlignment) size = LayoutHelper.ApplyLayoutConstraints(Child, size);
if (useLayoutRounding)
{
size = new Size(
Math.Ceiling(size.Width * scale) / scale,
Math.Ceiling(size.Height * scale) / scale);
availableSizeMinusMargins = new Size(
Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
}
switch (horizontalContentAlignment)
{ {
case HorizontalAlignment.Stretch:
case HorizontalAlignment.Center: case HorizontalAlignment.Center:
originX += (sizeMinusPadding.Width - size.Width) / 2; case HorizontalAlignment.Stretch:
originX += (availableSizeMinusMargins.Width - size.Width) / 2;
break; break;
case HorizontalAlignment.Right: case HorizontalAlignment.Right:
originX = size.Width - child.DesiredSize.Width; originX += availableSizeMinusMargins.Width - size.Width;
break; break;
} }
switch (verticalAlignment) switch (verticalContentAlignment)
{ {
case VerticalAlignment.Stretch:
case VerticalAlignment.Center: case VerticalAlignment.Center:
originY += (sizeMinusPadding.Height - size.Height) / 2; case VerticalAlignment.Stretch:
originY += (availableSizeMinusMargins.Height - size.Height) / 2;
break; break;
case VerticalAlignment.Bottom: case VerticalAlignment.Bottom:
originY = size.Height - child.DesiredSize.Height; originY += availableSizeMinusMargins.Height - size.Height;
break; break;
} }
child.Arrange(new Rect(originX, originY, size.Width, size.Height)); if (useLayoutRounding)
{
originX = Math.Floor(originX * scale) / scale;
originY = Math.Floor(originY * scale) / scale;
}
Child.Arrange(new Rect(originX, originY, size.Width, size.Height));
} }
return finalSize; return finalSize;
} }
/// <summary> private double GetLayoutScale()
/// Called when the <see cref="Content"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void ContentChanged(AvaloniaPropertyChangedEventArgs e)
{ {
_createdChild = false; var result = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0;
if (((ILogical)this).IsAttachedToLogicalTree) if (result == 0 || double.IsNaN(result) || double.IsInfinity(result))
{ {
UpdateChild(); throw new Exception($"Invalid LayoutScaling returned from {VisualRoot.GetType()}");
}
else if (Child != null)
{
VisualChildren.Remove(Child);
LogicalChildren.Remove(Child);
Child = null;
_dataTemplate = null;
} }
InvalidateMeasure(); return result;
} }
private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e) private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e)

66
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -62,7 +62,6 @@ namespace Avalonia.Controls.Presenters
private bool _canHorizontallyScroll; private bool _canHorizontallyScroll;
private bool _canVerticallyScroll; private bool _canVerticallyScroll;
private Size _extent; private Size _extent;
private Size _measuredExtent;
private Vector _offset; private Vector _offset;
private IDisposable _logicalScrollSubscription; private IDisposable _logicalScrollSubscription;
private Size _viewport; private Size _viewport;
@ -199,65 +198,34 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/> /// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
var child = Child; if (_logicalScrollSubscription != null || Child == null)
if (child != null)
{ {
var measureSize = availableSize; return base.MeasureOverride(availableSize);
}
if (_logicalScrollSubscription == null)
{
measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
if (!CanHorizontallyScroll)
{
measureSize = measureSize.WithWidth(availableSize.Width);
}
if (!CanVerticallyScroll) var constraint = new Size(
{ CanHorizontallyScroll ? double.PositiveInfinity : availableSize.Width,
measureSize = measureSize.WithHeight(availableSize.Height); CanVerticallyScroll ? double.PositiveInfinity : availableSize.Height);
}
}
child.Measure(measureSize); Child.Measure(constraint);
var size = child.DesiredSize; return Child.DesiredSize.Constrain(availableSize);
_measuredExtent = size;
return size.Constrain(availableSize);
}
else
{
return Extent = new Size();
}
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable; if (_logicalScrollSubscription != null || Child == null)
var logicalScroll = _logicalScrollSubscription != null;
if (!logicalScroll)
{
Viewport = finalSize;
Extent = _measuredExtent;
if (child != null)
{
var size = new Size(
Math.Max(finalSize.Width, child.DesiredSize.Width),
Math.Max(finalSize.Height, child.DesiredSize.Height));
child.Arrange(new Rect((Point)(-Offset), size));
return finalSize;
}
}
else if (child != null)
{ {
child.Arrange(new Rect(finalSize)); return base.ArrangeOverride(finalSize);
return finalSize;
} }
return new Size(); var size = new Size(
CanHorizontallyScroll ? Math.Max(Child.DesiredSize.Width, finalSize.Width) : finalSize.Width,
CanVerticallyScroll ? Math.Max(Child.DesiredSize.Height, finalSize.Height) : finalSize.Height);
ArrangeOverrideImpl(size, -Offset);
Viewport = finalSize;
Extent = Child.Bounds.Size;
return finalSize;
} }
/// <inheritdoc/> /// <inheritdoc/>

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

@ -53,12 +53,13 @@ namespace Avalonia.Controls.Primitives
foreach (var child in Children) foreach (var child in Children)
{ {
var info = (AdornedElementInfo)child.GetValue(s_adornedElementInfoProperty); var info = child.GetValue(s_adornedElementInfoProperty);
if (info != null && info.Bounds.HasValue) if (info != null && info.Bounds.HasValue)
{ {
child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform); child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform);
child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute); child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute);
UpdateClip(child, info.Bounds.Value);
child.Arrange(info.Bounds.Value.Bounds); child.Arrange(info.Bounds.Value.Bounds);
} }
else else
@ -78,6 +79,19 @@ namespace Avalonia.Controls.Primitives
layer?.UpdateAdornedElement(adorner, adorned); layer?.UpdateAdornedElement(adorner, adorned);
} }
private void UpdateClip(IControl control, TransformedBounds bounds)
{
var clip = control.Clip as RectangleGeometry;
if (clip == null)
{
clip = new RectangleGeometry { Transform = new MatrixTransform() };
control.Clip = clip;
}
clip.Rect = bounds.Clip.TransformToAABB(-bounds.Transform);
}
private void ChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) private void ChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{ {
switch (e.Action) switch (e.Action)

37
src/Avalonia.Controls/TextBox.cs

@ -325,7 +325,7 @@ namespace Avalonia.Controls
string text = Text ?? string.Empty; string text = Text ?? string.Empty;
int caretIndex = CaretIndex; int caretIndex = CaretIndex;
bool movement = false; bool movement = false;
bool handled = true; bool handled = false;
var modifiers = e.Modifiers; var modifiers = e.Modifiers;
switch (e.Key) switch (e.Key)
@ -334,13 +334,14 @@ namespace Avalonia.Controls
if (modifiers == InputModifiers.Control) if (modifiers == InputModifiers.Control)
{ {
SelectAll(); SelectAll();
handled = true;
} }
break; break;
case Key.C: case Key.C:
if (modifiers == InputModifiers.Control) if (modifiers == InputModifiers.Control)
{ {
Copy(); Copy();
handled = true;
} }
break; break;
@ -349,6 +350,7 @@ namespace Avalonia.Controls
{ {
Copy(); Copy();
DeleteSelection(); DeleteSelection();
handled = true;
} }
break; break;
@ -356,19 +358,24 @@ namespace Avalonia.Controls
if (modifiers == InputModifiers.Control) if (modifiers == InputModifiers.Control)
{ {
Paste(); Paste();
handled = true;
} }
break; break;
case Key.Z: case Key.Z:
if (modifiers == InputModifiers.Control) if (modifiers == InputModifiers.Control)
{
_undoRedoHelper.Undo(); _undoRedoHelper.Undo();
handled = true;
}
break; break;
case Key.Y: case Key.Y:
if (modifiers == InputModifiers.Control) if (modifiers == InputModifiers.Control)
{
_undoRedoHelper.Redo(); _undoRedoHelper.Redo();
handled = true;
}
break; break;
case Key.Left: case Key.Left:
MoveHorizontal(-1, modifiers); MoveHorizontal(-1, modifiers);
@ -381,13 +388,11 @@ namespace Avalonia.Controls
break; break;
case Key.Up: case Key.Up:
MoveVertical(-1, modifiers); movement = MoveVertical(-1, modifiers);
movement = true;
break; break;
case Key.Down: case Key.Down:
MoveVertical(1, modifiers); movement = MoveVertical(1, modifiers);
movement = true;
break; break;
case Key.Home: case Key.Home:
@ -423,7 +428,7 @@ namespace Avalonia.Controls
CaretIndex -= removedCharacters; CaretIndex -= removedCharacters;
SelectionStart = SelectionEnd = CaretIndex; SelectionStart = SelectionEnd = CaretIndex;
} }
handled = true;
break; break;
case Key.Delete: case Key.Delete:
@ -447,13 +452,14 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters)); SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters));
} }
handled = true;
break; break;
case Key.Enter: case Key.Enter:
if (AcceptsReturn) if (AcceptsReturn)
{ {
HandleTextInput("\r\n"); HandleTextInput("\r\n");
handled = true;
} }
break; break;
@ -462,11 +468,11 @@ namespace Avalonia.Controls
if (AcceptsTab) if (AcceptsTab)
{ {
HandleTextInput("\t"); HandleTextInput("\t");
handled = true;
} }
else else
{ {
base.OnKeyDown(e); base.OnKeyDown(e);
handled = false;
} }
break; break;
@ -485,7 +491,7 @@ namespace Avalonia.Controls
SelectionStart = SelectionEnd = CaretIndex; SelectionStart = SelectionEnd = CaretIndex;
} }
if (handled) if (handled || movement)
{ {
e.Handled = true; e.Handled = true;
} }
@ -662,7 +668,7 @@ namespace Avalonia.Controls
} }
} }
private void MoveVertical(int count, InputModifiers modifiers) private bool MoveVertical(int count, InputModifiers modifiers)
{ {
var formattedText = _presenter.FormattedText; var formattedText = _presenter.FormattedText;
var lines = formattedText.GetLines().ToList(); var lines = formattedText.GetLines().ToList();
@ -677,6 +683,11 @@ namespace Avalonia.Controls
var point = new Point(rect.X, y + (count * (line.Height / 2))); var point = new Point(rect.X, y + (count * (line.Height / 2)));
var hit = formattedText.HitTestPoint(point); var hit = formattedText.HitTestPoint(point);
CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0); CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0);
return true;
}
else
{
return false;
} }
} }

14
src/Avalonia.Controls/Window.cs

@ -373,28 +373,28 @@ namespace Avalonia.Controls
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
var sizeToContent = SizeToContent; var sizeToContent = SizeToContent;
var size = ClientSize; var clientSize = ClientSize;
var desired = base.MeasureOverride(availableSize.Constrain(_maxPlatformClientSize)); Size constraint;
switch (sizeToContent) switch (sizeToContent)
{ {
case SizeToContent.Width: case SizeToContent.Width:
size = new Size(desired.Width, ClientSize.Height); constraint = new Size(double.PositiveInfinity, ClientSize.Height);
break; break;
case SizeToContent.Height: case SizeToContent.Height:
size = new Size(ClientSize.Width, desired.Height); constraint = new Size(ClientSize.Width, double.PositiveInfinity);
break; break;
case SizeToContent.WidthAndHeight: case SizeToContent.WidthAndHeight:
size = new Size(desired.Width, desired.Height); constraint = Size.Infinity;
break; break;
case SizeToContent.Manual: case SizeToContent.Manual:
size = ClientSize; constraint = ClientSize;
break; break;
default: default:
throw new InvalidOperationException("Invalid value for SizeToContent."); throw new InvalidOperationException("Invalid value for SizeToContent.");
} }
return size; return base.MeasureOverride(constraint);
} }
protected override void HandleClosed() protected override void HandleClosed()

31
src/Avalonia.Visuals/Media/EllipseGeometry.cs

@ -17,15 +17,9 @@ namespace Avalonia.Media
public static readonly StyledProperty<Rect> RectProperty = public static readonly StyledProperty<Rect> RectProperty =
AvaloniaProperty.Register<EllipseGeometry, Rect>(nameof(Rect)); AvaloniaProperty.Register<EllipseGeometry, Rect>(nameof(Rect));
public Rect Rect
{
get => GetValue(RectProperty);
set => SetValue(RectProperty, value);
}
static EllipseGeometry() static EllipseGeometry()
{ {
RectProperty.Changed.AddClassHandler<EllipseGeometry>(x => x.RectChanged); AffectsGeometry(RectProperty);
} }
/// <summary> /// <summary>
@ -33,8 +27,6 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public EllipseGeometry() public EllipseGeometry()
{ {
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
PlatformImpl = factory.CreateStreamGeometry();
} }
/// <summary> /// <summary>
@ -46,17 +38,30 @@ namespace Avalonia.Media
Rect = rect; Rect = rect;
} }
/// <summary>
/// Gets or sets a rect that defines the bounds of the ellipse.
/// </summary>
public Rect Rect
{
get => GetValue(RectProperty);
set => SetValue(RectProperty, value);
}
/// <inheritdoc/> /// <inheritdoc/>
public override Geometry Clone() public override Geometry Clone()
{ {
return new EllipseGeometry(Rect); return new EllipseGeometry(Rect);
} }
private void RectChanged(AvaloniaPropertyChangedEventArgs e) /// <inheritdoc/>
protected override IGeometryImpl CreateDefiningGeometry()
{ {
var rect = (Rect)e.NewValue; var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
using (var ctx = ((IStreamGeometryImpl)PlatformImpl).Open()) var geometry = factory.CreateStreamGeometry();
using (var ctx = geometry.Open())
{ {
var rect = Rect;
double controlPointRatio = (Math.Sqrt(2) - 1) * 4 / 3; double controlPointRatio = (Math.Sqrt(2) - 1) * 4 / 3;
var center = rect.Center; var center = rect.Center;
var radius = new Vector(rect.Width / 2, rect.Height / 2); var radius = new Vector(rect.Width / 2, rect.Height / 2);
@ -80,6 +85,8 @@ namespace Avalonia.Media
ctx.CubicBezierTo(new Point(x0, y1), new Point(x1, y0), new Point(x2, y0)); ctx.CubicBezierTo(new Point(x0, y1), new Point(x1, y0), new Point(x2, y0));
ctx.EndFigure(true); ctx.EndFigure(true);
} }
return geometry;
} }
} }
} }

125
src/Avalonia.Visuals/Media/Geometry.cs

@ -17,26 +17,47 @@ namespace Avalonia.Media
public static readonly StyledProperty<Transform> TransformProperty = public static readonly StyledProperty<Transform> TransformProperty =
AvaloniaProperty.Register<Geometry, Transform>(nameof(Transform)); AvaloniaProperty.Register<Geometry, Transform>(nameof(Transform));
/// <summary> private bool _isDirty = true;
/// Initializes static members of the <see cref="Geometry"/> class. private IGeometryImpl _platformImpl;
/// </summary>
static Geometry() static Geometry()
{ {
TransformProperty.Changed.AddClassHandler<Geometry>(x => x.TransformChanged); TransformProperty.Changed.AddClassHandler<Geometry>(x => x.TransformChanged);
} }
/// <summary>
/// Raised when the geometry changes.
/// </summary>
public event EventHandler Changed;
/// <summary> /// <summary>
/// Gets the geometry's bounding rectangle. /// Gets the geometry's bounding rectangle.
/// </summary> /// </summary>
public Rect Bounds => PlatformImpl.Bounds; public Rect Bounds => PlatformImpl?.Bounds ?? Rect.Empty;
/// <summary> /// <summary>
/// Gets the platform-specific implementation of the geometry. /// Gets the platform-specific implementation of the geometry.
/// </summary> /// </summary>
public virtual IGeometryImpl PlatformImpl public IGeometryImpl PlatformImpl
{ {
get; get
protected set; {
if (_isDirty)
{
var geometry = CreateDefiningGeometry();
var transform = Transform;
if (geometry != null && transform != null && transform.Value != Matrix.Identity)
{
geometry = geometry.WithTransform(transform.Value);
}
_platformImpl = geometry;
_isDirty = false;
}
return _platformImpl;
}
} }
/// <summary> /// <summary>
@ -55,14 +76,11 @@ namespace Avalonia.Media
public abstract Geometry Clone(); public abstract Geometry Clone();
/// <summary> /// <summary>
/// Gets the geometry's bounding rectangle with the specified stroke thickness. /// Gets the geometry's bounding rectangle with the specified pen.
/// </summary> /// </summary>
/// <param name="strokeThickness">The stroke thickness.</param> /// <param name="pen">The stroke thickness.</param>
/// <returns>The bounding rectangle.</returns> /// <returns>The bounding rectangle.</returns>
public Rect GetRenderBounds(double strokeThickness) public Rect GetRenderBounds(Pen pen) => PlatformImpl?.GetRenderBounds(pen) ?? Rect.Empty;
{
return PlatformImpl.GetRenderBounds(strokeThickness);
}
/// <summary> /// <summary>
/// Indicates whether the geometry's fill contains the specified point. /// Indicates whether the geometry's fill contains the specified point.
@ -71,7 +89,7 @@ namespace Avalonia.Media
/// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns> /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
public bool FillContains(Point point) public bool FillContains(Point point)
{ {
return PlatformImpl.FillContains(point); return PlatformImpl?.FillContains(point) == true;
} }
/// <summary> /// <summary>
@ -82,13 +100,86 @@ namespace Avalonia.Media
/// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns> /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
public bool StrokeContains(Pen pen, Point point) public bool StrokeContains(Pen pen, Point point)
{ {
return PlatformImpl.StrokeContains(pen, point); return PlatformImpl?.StrokeContains(pen, point) == true;
}
/// <summary>
/// Marks a property as affecting the geometry's <see cref="PlatformImpl"/>.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a control's static constructor, any change to the
/// property will cause <see cref="InvalidateGeometry"/> to be called on the element.
/// </remarks>
protected static void AffectsGeometry(params AvaloniaProperty[] properties)
{
foreach (var property in properties)
{
property.Changed.Subscribe(AffectsGeometryInvalidate);
}
}
/// <summary>
/// Creates the platform implementation of the geometry, without the transform applied.
/// </summary>
/// <returns></returns>
protected abstract IGeometryImpl CreateDefiningGeometry();
/// <summary>
/// Invalidates the platform implementation of the geometry.
/// </summary>
protected void InvalidateGeometry()
{
_isDirty = true;
_platformImpl = null;
Changed?.Invoke(this, EventArgs.Empty);
} }
private void TransformChanged(AvaloniaPropertyChangedEventArgs e) private void TransformChanged(AvaloniaPropertyChangedEventArgs e)
{ {
var transform = (Transform)e.NewValue; var oldValue = (Transform)e.OldValue;
PlatformImpl = PlatformImpl.WithTransform(transform.Value); var newValue = (Transform)e.NewValue;
if (oldValue != null)
{
oldValue.Changed -= TransformChanged;
}
if (newValue != null)
{
newValue.Changed += TransformChanged;
}
TransformChanged(newValue, EventArgs.Empty);
}
private void TransformChanged(object sender, EventArgs e)
{
var transform = ((Transform)sender)?.Value;
if (_platformImpl is ITransformedGeometryImpl t)
{
if (transform == null || transform == Matrix.Identity)
{
_platformImpl = t.SourceGeometry;
}
else if (transform != t.Transform)
{
_platformImpl = t.SourceGeometry.WithTransform(transform.Value);
}
}
else if (_platformImpl != null && transform != null && transform != Matrix.Identity)
{
_platformImpl = PlatformImpl.WithTransform(transform.Value);
}
Changed?.Invoke(this, EventArgs.Empty);
}
private static void AffectsGeometryInvalidate(AvaloniaPropertyChangedEventArgs e)
{
var control = e.Sender as Geometry;
control?.InvalidateGeometry();
} }
} }
} }

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

@ -37,7 +37,8 @@
public override Rect GetBounds() public override Rect GetBounds()
{ {
// adding the Pen's stroke thickness here could yield wrong results due to transforms // adding the Pen's stroke thickness here could yield wrong results due to transforms
return Geometry?.GetRenderBounds(0) ?? new Rect(); var pen = new Pen(Brushes.Black, 0);
return Geometry?.GetRenderBounds(pen) ?? new Rect();
} }
} }
} }

66
src/Avalonia.Visuals/Media/LineGeometry.cs

@ -16,29 +16,15 @@ namespace Avalonia.Media
public static readonly StyledProperty<Point> StartPointProperty = public static readonly StyledProperty<Point> StartPointProperty =
AvaloniaProperty.Register<LineGeometry, Point>(nameof(StartPoint)); AvaloniaProperty.Register<LineGeometry, Point>(nameof(StartPoint));
public Point StartPoint
{
get => GetValue(StartPointProperty);
set => SetValue(StartPointProperty, value);
}
/// <summary> /// <summary>
/// Defines the <see cref="EndPoint"/> property. /// Defines the <see cref="EndPoint"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Point> EndPointProperty = public static readonly StyledProperty<Point> EndPointProperty =
AvaloniaProperty.Register<LineGeometry, Point>(nameof(EndPoint)); AvaloniaProperty.Register<LineGeometry, Point>(nameof(EndPoint));
private bool _isDirty = true;
public Point EndPoint
{
get => GetValue(EndPointProperty);
set => SetValue(EndPointProperty, value);
}
static LineGeometry() static LineGeometry()
{ {
StartPointProperty.Changed.AddClassHandler<LineGeometry>(x => x.PointsChanged); AffectsGeometry(StartPointProperty, EndPointProperty);
EndPointProperty.Changed.AddClassHandler<LineGeometry>(x => x.PointsChanged);
} }
/// <summary> /// <summary>
@ -46,8 +32,6 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public LineGeometry() public LineGeometry()
{ {
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
PlatformImpl = factory.CreateStreamGeometry();
} }
/// <summary> /// <summary>
@ -61,38 +45,44 @@ namespace Avalonia.Media
EndPoint = endPoint; EndPoint = endPoint;
} }
public override IGeometryImpl PlatformImpl /// <summary>
/// Gets or sets the start point of the line.
/// </summary>
public Point StartPoint
{ {
get get => GetValue(StartPointProperty);
{ set => SetValue(StartPointProperty, value);
PrepareIfNeeded();
return base.PlatformImpl;
}
protected set => base.PlatformImpl = value;
} }
public void PrepareIfNeeded() /// <summary>
/// Gets or sets the end point of the line.
/// </summary>
public Point EndPoint
{ {
if (_isDirty) get => GetValue(EndPointProperty);
{ set => SetValue(EndPointProperty, value);
_isDirty = false;
using (var context = ((IStreamGeometryImpl)PlatformImpl).Open())
{
context.BeginFigure(StartPoint, false);
context.LineTo(EndPoint);
context.EndFigure(false);
}
}
} }
/// <inheritdoc/> /// <inheritdoc/>
public override Geometry Clone() public override Geometry Clone()
{ {
PrepareIfNeeded();
return new LineGeometry(StartPoint, EndPoint); return new LineGeometry(StartPoint, EndPoint);
} }
private void PointsChanged(AvaloniaPropertyChangedEventArgs e) => _isDirty = true; /// <inheritdoc/>
protected override IGeometryImpl CreateDefiningGeometry()
{
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
var geometry = factory.CreateStreamGeometry();
using (var context = geometry.Open())
{
context.BeginFigure(StartPoint, false);
context.LineTo(EndPoint);
context.EndFigure(false);
}
return geometry;
}
} }
} }

68
src/Avalonia.Visuals/Media/PathGeometry.cs

@ -1,10 +1,10 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
using System;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -22,12 +22,14 @@ namespace Avalonia.Media
public static readonly StyledProperty<FillRule> FillRuleProperty = public static readonly StyledProperty<FillRule> FillRuleProperty =
AvaloniaProperty.Register<PathGeometry, FillRule>(nameof(FillRule)); AvaloniaProperty.Register<PathGeometry, FillRule>(nameof(FillRule));
private PathFigures _figures;
private IDisposable _figuresObserver;
private IDisposable _figuresPropertiesObserver;
static PathGeometry() static PathGeometry()
{ {
FiguresProperty.Changed.Subscribe(onNext: v => FiguresProperty.Changed.AddClassHandler<PathGeometry>((s, e) =>
{ s.OnFiguresChanged(e.NewValue as PathFigures));
(v.Sender as PathGeometry)?.OnFiguresChanged(v.OldValue as PathFigures, v.NewValue as PathFigures);
});
} }
/// <summary> /// <summary>
@ -63,61 +65,33 @@ namespace Avalonia.Media
set { SetValue(FillRuleProperty, value); } set { SetValue(FillRuleProperty, value); }
} }
public override IGeometryImpl PlatformImpl protected override IGeometryImpl CreateDefiningGeometry()
{ {
get var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
{ var geometry = factory.CreateStreamGeometry();
PrepareIfNeeded();
return base.PlatformImpl;
}
protected set using (var ctx = new StreamGeometryContext(geometry.Open()))
{ {
base.PlatformImpl = value; ctx.SetFillRule(FillRule);
} foreach (var f in Figures)
}
public override Geometry Clone()
{
PrepareIfNeeded();
return base.Clone();
}
public void PrepareIfNeeded()
{
if (_isDirty)
{
_isDirty = false;
using (var ctx = Open())
{ {
ctx.SetFillRule(FillRule); f.ApplyTo(ctx);
foreach (var f in Figures)
{
f.ApplyTo(ctx);
}
} }
} }
}
internal void NotifyChanged() return geometry;
{
_isDirty = true;
} }
private PathFigures _figures; private void OnFiguresChanged(PathFigures figures)
private IDisposable _figuresObserver = null;
private IDisposable _figuresPropertiesObserver = null;
private bool _isDirty = true;
private void OnFiguresChanged(PathFigures oldValue, PathFigures newValue)
{ {
_figuresObserver?.Dispose(); _figuresObserver?.Dispose();
_figuresPropertiesObserver?.Dispose(); _figuresPropertiesObserver?.Dispose();
_figuresObserver = newValue?.ForEachItem(f => NotifyChanged(), f => NotifyChanged(), () => NotifyChanged()); _figuresObserver = figures?.ForEachItem(
_figuresPropertiesObserver = newValue?.TrackItemPropertyChanged(t => NotifyChanged()); _ => InvalidateGeometry(),
_ => InvalidateGeometry(),
() => InvalidateGeometry());
_figuresPropertiesObserver = figures?.TrackItemPropertyChanged(_ => InvalidateGeometry());
} }
} }
} }

73
src/Avalonia.Visuals/Media/PolylineGeometry.cs

@ -27,14 +27,12 @@ namespace Avalonia.Media
AvaloniaProperty.Register<PolylineGeometry, bool>(nameof(IsFilled)); AvaloniaProperty.Register<PolylineGeometry, bool>(nameof(IsFilled));
private Points _points; private Points _points;
private bool _isDirty = true;
private IDisposable _pointsObserver; private IDisposable _pointsObserver;
static PolylineGeometry() static PolylineGeometry()
{ {
PointsProperty.Changed.AddClassHandler<PolylineGeometry>((s, e) => AffectsGeometry(IsFilledProperty);
s.OnPointsChanged(e.OldValue as Points, e.NewValue as Points)); PointsProperty.Changed.AddClassHandler<PolylineGeometry>((s, e) => s.OnPointsChanged(e.NewValue as Points));
IsFilledProperty.Changed.AddClassHandler<PolylineGeometry>((s, _) => s.NotifyChanged());
} }
/// <summary> /// <summary>
@ -42,9 +40,6 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public PolylineGeometry() public PolylineGeometry()
{ {
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
PlatformImpl = factory.CreateStreamGeometry();
Points = new Points(); Points = new Points();
} }
@ -57,29 +52,6 @@ namespace Avalonia.Media
IsFilled = isFilled; IsFilled = isFilled;
} }
public void PrepareIfNeeded()
{
if (_isDirty)
{
_isDirty = false;
using (var context = ((IStreamGeometryImpl)PlatformImpl).Open())
{
var points = Points;
var isFilled = IsFilled;
if (points.Count > 0)
{
context.BeginFigure(points[0], isFilled);
for (int i = 1; i < points.Count; i++)
{
context.LineTo(points[i]);
}
context.EndFigure(isFilled);
}
}
}
}
/// <summary> /// <summary>
/// Gets or sets the figures. /// Gets or sets the figures.
/// </summary> /// </summary>
@ -99,33 +71,42 @@ namespace Avalonia.Media
set => SetValue(IsFilledProperty, value); set => SetValue(IsFilledProperty, value);
} }
public override IGeometryImpl PlatformImpl
{
get
{
PrepareIfNeeded();
return base.PlatformImpl;
}
protected set => base.PlatformImpl = value;
}
/// <inheritdoc/> /// <inheritdoc/>
public override Geometry Clone() public override Geometry Clone()
{ {
PrepareIfNeeded();
return new PolylineGeometry(Points, IsFilled); return new PolylineGeometry(Points, IsFilled);
} }
private void OnPointsChanged(Points oldValue, Points newValue) protected override IGeometryImpl CreateDefiningGeometry()
{ {
_pointsObserver?.Dispose(); var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
var geometry = factory.CreateStreamGeometry();
using (var context = geometry.Open())
{
var points = Points;
var isFilled = IsFilled;
if (points.Count > 0)
{
context.BeginFigure(points[0], isFilled);
for (int i = 1; i < points.Count; i++)
{
context.LineTo(points[i]);
}
context.EndFigure(isFilled);
}
}
_pointsObserver = newValue?.ForEachItem(f => NotifyChanged(), f => NotifyChanged(), () => NotifyChanged()); return geometry;
} }
internal void NotifyChanged() private void OnPointsChanged(Points newValue)
{ {
_isDirty = true; _pointsObserver?.Dispose();
_pointsObserver = newValue?.ForEachItem(
_ => InvalidateGeometry(),
_ => InvalidateGeometry(),
InvalidateGeometry);
} }
} }
} }

22
src/Avalonia.Visuals/Media/RectangleGeometry.cs

@ -24,7 +24,7 @@ namespace Avalonia.Media
static RectangleGeometry() static RectangleGeometry()
{ {
RectProperty.Changed.AddClassHandler<RectangleGeometry>(x => x.RectChanged); AffectsGeometry(RectProperty);
} }
/// <summary> /// <summary>
@ -32,36 +32,36 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public RectangleGeometry() public RectangleGeometry()
{ {
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
PlatformImpl = factory.CreateStreamGeometry();
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RectangleGeometry"/> class. /// Initializes a new instance of the <see cref="RectangleGeometry"/> class.
/// </summary> /// </summary>
/// <param name="rect">The rectangle bounds.</param> /// <param name="rect">The rectangle bounds.</param>
public RectangleGeometry(Rect rect) : this() public RectangleGeometry(Rect rect)
{ {
Rect = rect; Rect = rect;
} }
/// <inheritdoc/> /// <inheritdoc/>
public override Geometry Clone() public override Geometry Clone() => new RectangleGeometry(Rect);
{
return new RectangleGeometry(Rect);
}
private void RectChanged(AvaloniaPropertyChangedEventArgs e) protected override IGeometryImpl CreateDefiningGeometry()
{ {
var rect = (Rect)e.NewValue; var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
using (var context = ((IStreamGeometryImpl)PlatformImpl).Open()) var geometry = factory.CreateStreamGeometry();
using (var context = geometry.Open())
{ {
var rect = Rect;
context.BeginFigure(rect.TopLeft, true); context.BeginFigure(rect.TopLeft, true);
context.LineTo(rect.TopRight); context.LineTo(rect.TopRight);
context.LineTo(rect.BottomRight); context.LineTo(rect.BottomRight);
context.LineTo(rect.BottomLeft); context.LineTo(rect.BottomLeft);
context.EndFigure(true); context.EndFigure(true);
} }
return geometry;
} }
} }
} }

20
src/Avalonia.Visuals/Media/StreamGeometry.cs

@ -10,22 +10,22 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public class StreamGeometry : Geometry public class StreamGeometry : Geometry
{ {
IStreamGeometryImpl _impl;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StreamGeometry"/> class. /// Initializes a new instance of the <see cref="StreamGeometry"/> class.
/// </summary> /// </summary>
public StreamGeometry() public StreamGeometry()
{ {
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
PlatformImpl = factory.CreateStreamGeometry();
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StreamGeometry"/> class. /// Initializes a new instance of the <see cref="StreamGeometry"/> class.
/// </summary> /// </summary>
/// <param name="impl">The platform-specific implementation.</param> /// <param name="impl">The platform-specific implementation.</param>
private StreamGeometry(IGeometryImpl impl) private StreamGeometry(IStreamGeometryImpl impl)
{ {
PlatformImpl = impl; _impl = impl;
} }
/// <summary> /// <summary>
@ -61,5 +61,17 @@ namespace Avalonia.Media
{ {
return new StreamGeometryContext(((IStreamGeometryImpl)PlatformImpl).Open()); return new StreamGeometryContext(((IStreamGeometryImpl)PlatformImpl).Open());
} }
/// <inheritdoc/>
protected override IGeometryImpl CreateDefiningGeometry()
{
if (_impl == null)
{
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
_impl = factory.CreateStreamGeometry();
}
return _impl;
}
} }
} }

16
src/Avalonia.Visuals/Platform/IGeometryImpl.cs

@ -1,12 +1,13 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Media; using Avalonia.Media;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
/// <summary> /// <summary>
/// Defines the platform-specific interface for <see cref="Avalonia.Media.Geometry"/>. /// Defines the platform-specific interface for a <see cref="Geometry"/>.
/// </summary> /// </summary>
public interface IGeometryImpl public interface IGeometryImpl
{ {
@ -16,16 +17,11 @@ namespace Avalonia.Platform
Rect Bounds { get; } Rect Bounds { get; }
/// <summary> /// <summary>
/// Gets the transform to applied to the geometry. /// Gets the geometry's bounding rectangle with the specified pen.
/// </summary> /// </summary>
Matrix Transform { get; } /// <param name="pen">The pen to use. May be null.</param>
/// <summary>
/// Gets the geometry's bounding rectangle with the specified stroke thickness.
/// </summary>
/// <param name="strokeThickness">The stroke thickness.</param>
/// <returns>The bounding rectangle.</returns> /// <returns>The bounding rectangle.</returns>
Rect GetRenderBounds(double strokeThickness); Rect GetRenderBounds(Pen pen);
/// <summary> /// <summary>
/// Indicates whether the geometry's fill contains the specified point. /// Indicates whether the geometry's fill contains the specified point.
@ -54,6 +50,6 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
/// <param name="transform">The transform.</param> /// <param name="transform">The transform.</param>
/// <returns>The cloned geometry.</returns> /// <returns>The cloned geometry.</returns>
IGeometryImpl WithTransform(Matrix transform); ITransformedGeometryImpl WithTransform(Matrix transform);
} }
} }

24
src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs

@ -0,0 +1,24 @@
using System;
namespace Avalonia.Platform
{
/// <summary>
/// Represents a geometry with a transform applied.
/// </summary>
/// <remarks>
/// An <see cref="ITransformedGeometryImpl"/> transforms a geometry without transforming its
/// stroke thickness.
/// </remarks>
public interface ITransformedGeometryImpl : IGeometryImpl
{
/// <summary>
/// Gets the source geometry that the <see cref="Transform"/> is applied to.
/// </summary>
IGeometryImpl SourceGeometry { get; }
/// <summary>
/// Gets the applied transform.
/// </summary>
Matrix Transform { get; }
}
}

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

@ -28,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph
Pen pen, Pen pen,
IGeometryImpl geometry, IGeometryImpl geometry,
IDictionary<IVisual, Scene> childScenes = null) IDictionary<IVisual, Scene> childScenes = null)
: base(geometry.GetRenderBounds(pen?.Thickness ?? 0), transform, null) : base(geometry.GetRenderBounds(pen), transform, null)
{ {
Transform = transform; Transform = transform;
Brush = brush?.ToImmutable(); Brush = brush?.ToImmutable();

6
src/Avalonia.Visuals/VisualTree/TransformedBounds.cs

@ -24,17 +24,17 @@ namespace Avalonia.VisualTree
} }
/// <summary> /// <summary>
/// Gets the control's bounds. /// Gets the control's bounds in its local coordinate space.
/// </summary> /// </summary>
public Rect Bounds { get; } public Rect Bounds { get; }
/// <summary> /// <summary>
/// Gets the control's clip rectangle. /// Gets the control's clip rectangle in global coordinate space.
/// </summary> /// </summary>
public Rect Clip { get; } public Rect Clip { get; }
/// <summary> /// <summary>
/// Gets the control's transform. /// Gets the transform from local to global coordinate space.
/// </summary> /// </summary>
public Matrix Transform { get; } public Matrix Transform { get; }

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

@ -69,7 +69,7 @@ namespace Avalonia.Skia
public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
{ {
var impl = (StreamGeometryImpl)geometry; var impl = (GeometryImpl)geometry;
var size = geometry.Bounds.Size; var size = geometry.Bounds.Size;
using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))

18
src/Skia/Avalonia.Skia/GeometryImpl.cs

@ -0,0 +1,18 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia
{
abstract class GeometryImpl : IGeometryImpl
{
public abstract Rect Bounds { get; }
public abstract SKPath EffectivePath { get; }
public abstract bool FillContains(Point point);
public abstract Rect GetRenderBounds(Pen pen);
public abstract IGeometryImpl Intersect(IGeometryImpl geometry);
public abstract bool StrokeContains(Pen pen, Point point);
public abstract ITransformedGeometryImpl WithTransform(Matrix transform);
}
}

56
src/Skia/Avalonia.Skia/StreamGeometryImpl.cs

@ -1,43 +1,30 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.RenderHelpers;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
class StreamGeometryImpl : IStreamGeometryImpl class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl
{ {
Rect _bounds;
SKPath _path; SKPath _path;
private Matrix _transform = Matrix.Identity; public override SKPath EffectivePath => _path;
public SKPath EffectivePath => _path; public override Rect GetRenderBounds(Pen pen)
public Rect GetRenderBounds(double strokeThickness)
{ {
// TODO: Calculate properly. return GetRenderBounds(pen?.Thickness ?? 0);
return Bounds.TransformToAABB(Transform).Inflate(strokeThickness);
} }
public Rect Bounds { get; private set; } public override Rect Bounds => _bounds;
public Matrix Transform
{
get { return _transform; }
}
public IStreamGeometryImpl Clone() public IStreamGeometryImpl Clone()
{ {
return new StreamGeometryImpl return new StreamGeometryImpl
{ {
_path = _path?.Clone(), _path = _path?.Clone(),
_transform = Transform, _bounds = Bounds
Bounds = Bounds
}; };
} }
@ -49,41 +36,34 @@ namespace Avalonia.Skia
return new StreamContext(this); return new StreamContext(this);
} }
public bool FillContains(Point point) public override bool FillContains(Point point)
{ {
// TODO: Not supported by SkiaSharp yet, so use expanded Rect // TODO: Not supported by SkiaSharp yet, so use expanded Rect
// return EffectivePath.Contains(point.X, point.Y); // return EffectivePath.Contains(point.X, point.Y);
return GetRenderBounds(0).Contains(point); return GetRenderBounds(0).Contains(point);
} }
public bool StrokeContains(Pen pen, Point point) public override bool StrokeContains(Pen pen, Point point)
{ {
// TODO: Not supported by SkiaSharp yet, so use expanded Rect // TODO: Not supported by SkiaSharp yet, so use expanded Rect
// return EffectivePath.Contains(point.X, point.Y); // return EffectivePath.Contains(point.X, point.Y);
return GetRenderBounds(0).Contains(point); return GetRenderBounds(0).Contains(point);
} }
public IGeometryImpl Intersect(IGeometryImpl geometry) public override IGeometryImpl Intersect(IGeometryImpl geometry)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IGeometryImpl WithTransform(Matrix transform) public override ITransformedGeometryImpl WithTransform(Matrix transform)
{ {
var result = (StreamGeometryImpl)Clone(); return new TransformedGeometryImpl(this, transform);
}
if (result.Transform != Matrix.Identity)
{
result._path.Transform(result.Transform.Invert().ToSKMatrix());
}
if (transform != Matrix.Identity)
{
result._path.Transform(transform.ToSKMatrix());
}
result._transform = transform; private Rect GetRenderBounds(double strokeThickness)
return result; {
// TODO: Calculate properly.
return Bounds.Inflate(strokeThickness);
} }
class StreamContext : IStreamGeometryContextImpl class StreamContext : IStreamGeometryContextImpl
@ -102,7 +82,7 @@ namespace Avalonia.Skia
{ {
SKRect rc; SKRect rc;
_path.GetBounds(out rc); _path.GetBounds(out rc);
_geometryImpl.Bounds = rc.ToAvaloniaRect(); _geometryImpl._bounds = rc.ToAvaloniaRect();
} }
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)

59
src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs

@ -0,0 +1,59 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia
{
class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl
{
public TransformedGeometryImpl(GeometryImpl source, Matrix transform)
{
SourceGeometry = source;
Transform = transform;
EffectivePath = source.EffectivePath.Clone();
EffectivePath.Transform(transform.ToSKMatrix());
}
public override SKPath EffectivePath { get; }
public IGeometryImpl SourceGeometry { get; }
public Matrix Transform { get; }
public override Rect Bounds => SourceGeometry.Bounds.TransformToAABB(Transform);
public override bool FillContains(Point point)
{
// TODO: Not supported by SkiaSharp yet, so use expanded Rect
return GetRenderBounds(0).Contains(point);
}
public override Rect GetRenderBounds(Pen pen)
{
return GetRenderBounds(pen.Thickness);
}
public override IGeometryImpl Intersect(IGeometryImpl geometry)
{
throw new NotImplementedException();
}
public override bool StrokeContains(Pen pen, Point point)
{
// TODO: Not supported by SkiaSharp yet, so use expanded Rect
return GetRenderBounds(0).Contains(point);
}
public override ITransformedGeometryImpl WithTransform(Matrix transform)
{
return new TransformedGeometryImpl(this, transform);
}
public Rect GetRenderBounds(double strokeThickness)
{
// TODO: Calculate properly.
return Bounds.Inflate(strokeThickness);
}
}
}

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

@ -189,7 +189,7 @@ namespace Avalonia.Direct2D1.Media
if (pen != null) if (pen != null)
{ {
using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen.Thickness).Size)) using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen).Size))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_renderTarget)) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_renderTarget))
{ {
if (d2dBrush.PlatformBrush != null) if (d2dBrush.PlatformBrush != null)

14
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@ -24,12 +24,10 @@ namespace Avalonia.Direct2D1.Media
public Geometry Geometry { get; } public Geometry Geometry { get; }
/// <inheritdoc/> /// <inheritdoc/>
public virtual Matrix Transform => Matrix.Identity; public Rect GetRenderBounds(Avalonia.Media.Pen pen)
/// <inheritdoc/>
public Rect GetRenderBounds(double strokeThickness)
{ {
return Geometry.GetWidenedBounds((float)strokeThickness).ToAvalonia(); var factory = AvaloniaLocator.Current.GetService<Factory>();
return Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -56,15 +54,15 @@ namespace Avalonia.Direct2D1.Media
return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness); return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness);
} }
/// <inheritdoc/> public ITransformedGeometryImpl WithTransform(Matrix transform)
public IGeometryImpl WithTransform(Matrix transform)
{ {
var factory = AvaloniaLocator.Current.GetService<Factory>(); var factory = AvaloniaLocator.Current.GetService<Factory>();
return new TransformedGeometryImpl( return new TransformedGeometryImpl(
new TransformedGeometry( new TransformedGeometry(
factory, factory,
GetSourceGeometry(), GetSourceGeometry(),
transform.ToDirect2D())); transform.ToDirect2D()),
this);
} }
protected virtual Geometry GetSourceGeometry() => Geometry; protected virtual Geometry GetSourceGeometry() => Geometry;

10
src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs

@ -1,23 +1,27 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Platform;
using SharpDX.Direct2D1; using SharpDX.Direct2D1;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
public class TransformedGeometryImpl : GeometryImpl public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class. /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary> /// </summary>
/// <param name="geometry">An existing Direct2D <see cref="TransformedGeometry"/>.</param> /// <param name="geometry">An existing Direct2D <see cref="TransformedGeometry"/>.</param>
public TransformedGeometryImpl(TransformedGeometry geometry) public TransformedGeometryImpl(TransformedGeometry geometry, GeometryImpl source)
: base(geometry) : base(geometry)
{ {
SourceGeometry = source;
} }
public IGeometryImpl SourceGeometry { get; }
/// <inheritdoc/> /// <inheritdoc/>
public override Matrix Transform => ((TransformedGeometry)Geometry).Transform.ToAvalonia(); public Matrix Transform => ((TransformedGeometry)Geometry).Transform.ToAvalonia();
protected override Geometry GetSourceGeometry() => ((TransformedGeometry)Geometry).SourceGeometry; protected override Geometry GetSourceGeometry() => ((TransformedGeometry)Geometry).SourceGeometry;
} }

15
src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs

@ -105,7 +105,18 @@ namespace Avalonia.Direct2D1
/// <param name="pen">The pen to convert.</param> /// <param name="pen">The pen to convert.</param>
/// <param name="target">The render target.</param> /// <param name="target">The render target.</param>
/// <returns>The Direct2D brush.</returns> /// <returns>The Direct2D brush.</returns>
public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, SharpDX.Direct2D1.RenderTarget target) public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, SharpDX.Direct2D1.RenderTarget renderTarget)
{
return pen.ToDirect2DStrokeStyle(renderTarget.Factory);
}
/// <summary>
/// Converts a pen to a Direct2D stroke style.
/// </summary>
/// <param name="pen">The pen to convert.</param>
/// <param name="target">The render target.</param>
/// <returns>The Direct2D brush.</returns>
public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, Factory factory)
{ {
var properties = new StrokeStyleProperties var properties = new StrokeStyleProperties
{ {
@ -123,7 +134,7 @@ namespace Avalonia.Direct2D1
properties.DashOffset = (float)pen.DashStyle.Offset; properties.DashOffset = (float)pen.DashStyle.Offset;
dashes = pen.DashStyle?.Dashes.Select(x => (float)x).ToArray(); dashes = pen.DashStyle?.Dashes.Select(x => (float)x).ToArray();
} }
return new StrokeStyle(target.Factory, properties, dashes); return new StrokeStyle(factory, properties, dashes);
} }
/// <summary> /// <summary>

194
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs

@ -0,0 +1,194 @@
// 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 Avalonia.Controls.Presenters;
using Avalonia.Layout;
using Xunit;
namespace Avalonia.Controls.UnitTests.Presenters
{
public class ContentPresenterTests_Layout
{
[Theory]
[InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Stretch, 0, 0, 100, 100)]
[InlineData(HorizontalAlignment.Left, VerticalAlignment.Stretch, 0, 0, 16, 100)]
[InlineData(HorizontalAlignment.Right, VerticalAlignment.Stretch, 84, 0, 16, 100)]
[InlineData(HorizontalAlignment.Center, VerticalAlignment.Stretch, 42, 0, 16, 100)]
[InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Top, 0, 0, 100, 16)]
[InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Bottom, 0, 84, 100, 16)]
[InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Center, 0, 42, 100, 16)]
public void Content_Alignment_Is_Applied_To_Child_Bounds(
HorizontalAlignment h,
VerticalAlignment v,
double expectedX,
double expectedY,
double expectedWidth,
double expectedHeight)
{
Border content;
var target = new ContentPresenter
{
HorizontalContentAlignment = h,
VerticalContentAlignment = v,
Content = content = new Border
{
MinWidth = 16,
MinHeight = 16,
},
};
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), content.Bounds);
}
[Theory]
[InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Stretch, 10, 10, 80, 80)]
[InlineData(HorizontalAlignment.Left, VerticalAlignment.Stretch, 10, 10, 16, 80)]
[InlineData(HorizontalAlignment.Right, VerticalAlignment.Stretch, 74, 10, 16, 80)]
[InlineData(HorizontalAlignment.Center, VerticalAlignment.Stretch, 42, 10, 16, 80)]
[InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Top, 10, 10, 80, 16)]
[InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Bottom, 10, 74, 80, 16)]
[InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Center, 10, 42, 80, 16)]
public void Content_Alignment_And_Padding_Are_Applied_To_Child_Bounds(
HorizontalAlignment h,
VerticalAlignment v,
double expectedX,
double expectedY,
double expectedWidth,
double expectedHeight)
{
Border content;
var target = new ContentPresenter
{
HorizontalContentAlignment = h,
VerticalContentAlignment = v,
Padding = new Thickness(10),
Content = content = new Border
{
MinWidth = 16,
MinHeight = 16,
},
};
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), content.Bounds);
}
[Fact]
public void Content_Can_Be_Stretched()
{
Border content;
var target = new ContentPresenter
{
Content = content = new Border
{
MinWidth = 16,
MinHeight = 16,
},
};
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(0, 0, 100, 100), content.Bounds);
}
[Fact]
public void Content_Can_Be_Right_Aligned()
{
Border content;
var target = new ContentPresenter
{
Content = content = new Border
{
MinWidth = 16,
MinHeight = 16,
HorizontalAlignment = HorizontalAlignment.Right
},
};
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(84, 0, 16, 100), content.Bounds);
}
[Fact]
public void Content_Can_Be_Bottom_Aligned()
{
Border content;
var target = new ContentPresenter
{
Content = content = new Border
{
MinWidth = 16,
MinHeight = 16,
VerticalAlignment = VerticalAlignment.Bottom,
},
};
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(0, 84, 100, 16), content.Bounds);
}
[Fact]
public void Content_Can_Be_TopLeft_Aligned()
{
Border content;
var target = new ContentPresenter
{
Content = content = new Border
{
MinWidth = 16,
MinHeight = 16,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
},
};
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(84, 0, 16, 16), content.Bounds);
}
[Fact]
public void Content_Can_Be_TopRight_Aligned()
{
Border content;
var target = new ContentPresenter
{
Content = content = new Border
{
MinWidth = 16,
MinHeight = 16,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
},
};
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(84, 0, 16, 16), content.Bounds);
}
[Fact]
public void Padding_Is_Applied_To_TopLeft_Aligned_Content()
{
}
}
}

4
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@ -204,7 +204,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
scroll.Arrange(new Rect(0, 0, 100, 100)); scroll.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(20, target.Panel.Children.Count); Assert.Equal(20, target.Panel.Children.Count);
Assert.Equal(new Size(10, 200), scroll.Extent); Assert.Equal(new Size(100, 200), scroll.Extent);
Assert.Equal(new Size(100, 100), scroll.Viewport); Assert.Equal(new Size(100, 100), scroll.Viewport);
target.VirtualizationMode = ItemVirtualizationMode.Simple; target.VirtualizationMode = ItemVirtualizationMode.Simple;
@ -266,7 +266,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
scroll.Arrange(new Rect(0, 0, 100, 100)); scroll.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(20, target.Panel.Children.Count); Assert.Equal(20, target.Panel.Children.Count);
Assert.Equal(new Size(10, 200), scroll.Extent); Assert.Equal(new Size(100, 200), scroll.Extent);
Assert.Equal(new Size(100, 100), scroll.Viewport); Assert.Equal(new Size(100, 100), scroll.Viewport);
} }

155
tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs

@ -13,55 +13,32 @@ namespace Avalonia.Controls.UnitTests.Presenters
{ {
public class ScrollContentPresenterTests public class ScrollContentPresenterTests
{ {
[Fact] [Theory]
public void Content_Can_Be_Left_Aligned() [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Stretch, 10, 10, 80, 80)]
{ [InlineData(HorizontalAlignment.Left, VerticalAlignment.Stretch, 10, 10, 16, 80)]
Border content; [InlineData(HorizontalAlignment.Right, VerticalAlignment.Stretch, 74, 10, 16, 80)]
var target = new ScrollContentPresenter [InlineData(HorizontalAlignment.Center, VerticalAlignment.Stretch, 42, 10, 16, 80)]
{ [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Top, 10, 10, 80, 16)]
Content = content = new Border [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Bottom, 10, 74, 80, 16)]
{ [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Center, 10, 42, 80, 16)]
Padding = new Thickness(8), public void Alignment_And_Padding_Are_Applied_To_Child_Bounds(
HorizontalAlignment = HorizontalAlignment.Left HorizontalAlignment h,
}, VerticalAlignment v,
}; double expectedX,
double expectedY,
target.UpdateChild(); double expectedWidth,
target.Measure(new Size(100, 100)); double expectedHeight)
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(0, 0, 16, 100), content.Bounds);
}
[Fact]
public void Content_Can_Be_Stretched()
{
Border content;
var target = new ScrollContentPresenter
{
Content = content = new Border
{
Padding = new Thickness(8),
},
};
target.UpdateChild();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(0, 0, 100, 100), content.Bounds);
}
[Fact]
public void Content_Can_Be_Right_Aligned()
{ {
Border content; Border content;
var target = new ScrollContentPresenter var target = new ScrollContentPresenter
{ {
Padding = new Thickness(10),
Content = content = new Border Content = content = new Border
{ {
Padding = new Thickness(8), MinWidth = 16,
HorizontalAlignment = HorizontalAlignment.Right MinHeight = 16,
HorizontalAlignment = h,
VerticalAlignment = v,
}, },
}; };
@ -69,19 +46,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(100, 100)); target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(84, 0, 16, 100), content.Bounds); Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), content.Bounds);
} }
[Fact] [Fact]
public void Content_Can_Be_Bottom_Aligned() public void DesiredSize_Is_Content_Size_When_Smaller_Than_AvailableSize()
{ {
Border content;
var target = new ScrollContentPresenter var target = new ScrollContentPresenter
{ {
Content = content = new Border Padding = new Thickness(10),
Content = new Border
{ {
Padding = new Thickness(8), MinWidth = 16,
VerticalAlignment = VerticalAlignment.Bottom, MinHeight = 16,
}, },
}; };
@ -89,20 +66,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(100, 100)); target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(0, 84, 100, 16), content.Bounds); Assert.Equal(new Size(16, 16), target.DesiredSize);
} }
[Fact] [Fact]
public void Content_Can_Be_TopRight_Aligned() public void DesiredSize_Is_AvailableSize_When_Content_Larger_Than_AvailableSize()
{ {
Border content;
var target = new ScrollContentPresenter var target = new ScrollContentPresenter
{ {
Content = content = new Border Padding = new Thickness(10),
Content = new Border
{ {
Padding = new Thickness(8), MinWidth = 160,
HorizontalAlignment = HorizontalAlignment.Right, MinHeight = 160,
VerticalAlignment = VerticalAlignment.Top,
}, },
}; };
@ -110,7 +86,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(100, 100)); target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(84, 0, 16, 16), content.Bounds); Assert.Equal(new Size(100, 100), target.DesiredSize);
} }
[Fact] [Fact]
@ -208,6 +184,71 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(new[] { "Viewport", "Extent" }, set); Assert.Equal(new[] { "Viewport", "Extent" }, set);
} }
[Fact]
public void Should_Correctly_Arrange_Child_Larger_Than_Viewport()
{
var child = new Canvas { MinWidth = 150, MinHeight = 150 };
var target = new ScrollContentPresenter { Content = child, };
target.UpdateChild();
target.Measure(Size.Infinity);
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Size(150, 150), child.Bounds.Size);
}
[Fact]
public void Arrange_Should_Constrain_Child_Width_When_CanHorizontallyScroll_False()
{
var child = new WrapPanel
{
Children =
{
new Border { Width = 40, Height = 50 },
new Border { Width = 40, Height = 50 },
new Border { Width = 40, Height = 50 },
}
};
var target = new ScrollContentPresenter
{
Content = child,
CanHorizontallyScroll = false,
};
target.UpdateChild();
target.Measure(Size.Infinity);
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(100, child.Bounds.Width);
}
[Fact]
public void Extent_Width_Should_Be_Arrange_Width_When_CanScrollHorizontally_False()
{
var child = new WrapPanel
{
Children =
{
new Border { Width = 40, Height = 50 },
new Border { Width = 40, Height = 50 },
new Border { Width = 40, Height = 50 },
}
};
var target = new ScrollContentPresenter
{
Content = child,
CanHorizontallyScroll = false,
};
target.UpdateChild();
target.Measure(Size.Infinity);
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Size(100, 100), target.Extent);
}
[Fact] [Fact]
public void Setting_Offset_Should_Invalidate_Arrange() public void Setting_Offset_Should_Invalidate_Arrange()
{ {

3
tests/Avalonia.Direct2D1.UnitTests/Media/GeometryTests.cs

@ -31,8 +31,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
Direct2D1Platform.Initialize(); Direct2D1Platform.Initialize();
var target = StreamGeometry.Parse("M 0 2 L 4 6 L 0 10 Z"); var target = StreamGeometry.Parse("M 0 2 L 4 6 L 0 10 Z");
var pen = new Pen(Brushes.Black, 2);
Assert.Equal(new Rect(-1, -0.414, 6.414, 12.828), target.GetRenderBounds(2), Compare); Assert.Equal(new Rect(-1, -0.414, 6.414, 12.828), target.GetRenderBounds(pen), Compare);
} }
} }
} }

110
tests/Avalonia.Layout.UnitTests/LayoutableTests.cs

@ -7,6 +7,97 @@ namespace Avalonia.Layout.UnitTests
{ {
public class LayoutableTests public class LayoutableTests
{ {
[Theory]
[InlineData(0, 0, 0, 0, 100, 100)]
[InlineData(10, 0, 0, 0, 90, 100)]
[InlineData(10, 0, 5, 0, 85, 100)]
[InlineData(0, 10, 0, 0, 100, 90)]
[InlineData(0, 10, 0, 5, 100, 85)]
[InlineData(4, 4, 6, 7, 90, 89)]
public void Margin_Is_Applied_To_MeasureOverride_Size(
double l,
double t,
double r,
double b,
double expectedWidth,
double expectedHeight)
{
var target = new TestLayoutable
{
Margin = new Thickness(l, t, r, b),
};
target.Measure(new Size(100, 100));
Assert.Equal(new Size(expectedWidth, expectedHeight), target.MeasureSize);
}
[Theory]
[InlineData(HorizontalAlignment.Stretch, 100)]
[InlineData(HorizontalAlignment.Left, 10)]
[InlineData(HorizontalAlignment.Center, 10)]
[InlineData(HorizontalAlignment.Right, 10)]
public void HorizontalAlignment_Is_Applied_To_ArrangeOverride_Size(
HorizontalAlignment h,
double expectedWidth)
{
var target = new TestLayoutable
{
HorizontalAlignment = h,
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Size(expectedWidth, 100), target.ArrangeSize);
}
[Theory]
[InlineData(VerticalAlignment.Stretch, 100)]
[InlineData(VerticalAlignment.Top, 10)]
[InlineData(VerticalAlignment.Center, 10)]
[InlineData(VerticalAlignment.Bottom, 10)]
public void VerticalAlignment_Is_Applied_To_ArrangeOverride_Size(
VerticalAlignment v,
double expectedHeight)
{
var target = new TestLayoutable
{
VerticalAlignment = v,
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Size(100, expectedHeight), target.ArrangeSize);
}
[Theory]
[InlineData(0, 0, 0, 0, 100, 100)]
[InlineData(10, 0, 0, 0, 90, 100)]
[InlineData(10, 0, 5, 0, 85, 100)]
[InlineData(0, 10, 0, 0, 100, 90)]
[InlineData(0, 10, 0, 5, 100, 85)]
[InlineData(4, 4, 6, 7, 90, 89)]
public void Margin_Is_Applied_To_ArrangeOverride_Size(
double l,
double t,
double r,
double b,
double expectedWidth,
double expectedHeight)
{
var target = new TestLayoutable
{
Margin = new Thickness(l, t, r, b),
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Size(expectedWidth, expectedHeight), target.ArrangeSize);
}
[Fact] [Fact]
public void Only_Calls_LayoutManager_InvalidateMeasure_Once() public void Only_Calls_LayoutManager_InvalidateMeasure_Once()
{ {
@ -86,5 +177,24 @@ namespace Avalonia.Layout.UnitTests
AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(layoutManager); AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(layoutManager);
return result; return result;
} }
private class TestLayoutable : Layoutable
{
public Size ArrangeSize { get; private set; }
public Size MeasureResult { get; set; } = new Size(10, 10);
public Size MeasureSize { get; private set; }
protected override Size MeasureOverride(Size availableSize)
{
MeasureSize = availableSize;
return MeasureResult;
}
protected override Size ArrangeOverride(Size finalSize)
{
ArrangeSize = finalSize;
return base.ArrangeOverride(finalSize);
}
}
} }
} }

12
tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs

@ -5,7 +5,7 @@ using Avalonia.Platform;
namespace Avalonia.UnitTests namespace Avalonia.UnitTests
{ {
public class MockStreamGeometryImpl : IStreamGeometryImpl public class MockStreamGeometryImpl : IStreamGeometryImpl, ITransformedGeometryImpl
{ {
private MockStreamGeometryContext _context; private MockStreamGeometryContext _context;
@ -27,6 +27,8 @@ namespace Avalonia.UnitTests
_context = context; _context = context;
} }
public IGeometryImpl SourceGeometry { get; }
public Rect Bounds => _context.CalculateBounds(); public Rect Bounds => _context.CalculateBounds();
public Matrix Transform { get; } public Matrix Transform { get; }
@ -36,6 +38,10 @@ namespace Avalonia.UnitTests
return this; return this;
} }
public void Dispose()
{
}
public bool FillContains(Point point) public bool FillContains(Point point)
{ {
return _context.FillContains(point); return _context.FillContains(point);
@ -46,7 +52,7 @@ namespace Avalonia.UnitTests
return false; return false;
} }
public Rect GetRenderBounds(double strokeThickness) => Bounds; public Rect GetRenderBounds(Pen pen) => Bounds;
public IGeometryImpl Intersect(IGeometryImpl geometry) public IGeometryImpl Intersect(IGeometryImpl geometry)
{ {
@ -58,7 +64,7 @@ namespace Avalonia.UnitTests
return _context; return _context;
} }
public IGeometryImpl WithTransform(Matrix transform) public ITransformedGeometryImpl WithTransform(Matrix transform)
{ {
return new MockStreamGeometryImpl(transform, _context); return new MockStreamGeometryImpl(transform, _context);
} }

115
tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs

@ -0,0 +1,115 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using Moq;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class GeometryTests
{
[Fact]
public void Changing_AffectsGeometry_Property_Causes_PlatformImpl_To_Be_Updated()
{
var target = new TestGeometry();
var platformImpl = target.PlatformImpl;
target.Foo = true;
Assert.NotSame(platformImpl, target.PlatformImpl);
}
[Fact]
public void Changing_AffectsGeometry_Property_Causes_Changed_To_Be_Raised()
{
var target = new TestGeometry();
var raised = false;
target.Changed += (s, e) => raised = true;
target.Foo = true;
Assert.True(raised);
}
[Fact]
public void Setting_Transform_Causes_Changed_To_Be_Raised()
{
var target = new TestGeometry();
var raised = false;
target.Changed += (s, e) => raised = true;
target.Transform = new RotateTransform(45);
Assert.True(raised);
}
[Fact]
public void Changing_Transform_Causes_Changed_To_Be_Raised()
{
var transform = new RotateTransform(45);
var target = new TestGeometry { Transform = transform };
var raised = false;
target.Changed += (s, e) => raised = true;
transform.Angle = 90;
Assert.True(raised);
}
[Fact]
public void Removing_Transform_Causes_Changed_To_Be_Raised()
{
var transform = new RotateTransform(45);
var target = new TestGeometry { Transform = transform };
var raised = false;
target.Changed += (s, e) => raised = true;
target.Transform = null;
Assert.True(raised);
}
[Fact]
public void Transform_Produces_Transformed_PlatformImpl()
{
var target = new TestGeometry();
var rotate = new RotateTransform(45);
Assert.False(target.PlatformImpl is ITransformedGeometryImpl);
target.Transform = rotate;
Assert.True(target.PlatformImpl is ITransformedGeometryImpl);
rotate.Angle = 0;
Assert.False(target.PlatformImpl is ITransformedGeometryImpl);
}
private class TestGeometry : Geometry
{
public static readonly AvaloniaProperty<bool> FooProperty =
AvaloniaProperty.Register<TestGeometry, bool>(nameof(Foo));
static TestGeometry()
{
AffectsGeometry(FooProperty);
}
public bool Foo
{
get => GetValue(FooProperty);
set => SetValue(FooProperty, value);
}
public override Geometry Clone()
{
throw new NotImplementedException();
}
protected override IGeometryImpl CreateDefiningGeometry()
{
return Mock.Of<IGeometryImpl>(
x => x.WithTransform(It.IsAny<Matrix>()) ==
Mock.Of<ITransformedGeometryImpl>(y =>
y.SourceGeometry == x));
}
}
}
}

39
tests/Avalonia.Visuals.UnitTests/Media/RectangleGeometryTests.cs

@ -0,0 +1,39 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class RectangleGeometryTests
{
[Fact]
public void Rectangle_With_Transform_Can_Be_Changed()
{
using (UnitTestApplication.Start(GetServices()))
{
var target = new RectangleGeometry
{
Rect = new Rect(0, 0, 100, 100),
Transform = new RotateTransform(45),
};
target.Rect = new Rect(50, 50, 150, 150);
}
}
private TestServices GetServices()
{
var context = Mock.Of<IStreamGeometryContextImpl>();
var transformedGeometry = new Mock<ITransformedGeometryImpl>();
var streamGeometry = Mock.Of<IStreamGeometryImpl>(x =>
x.Open() == context &&
x.WithTransform(It.IsAny<Matrix>()) == transformedGeometry.Object);
var renderInterface = Mock.Of<IPlatformRenderInterface>(x =>
x.CreateStreamGeometry() == streamGeometry);
return new TestServices(renderInterface: renderInterface);
}
}
}

19
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@ -65,22 +65,13 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
} }
} }
public Matrix Transform public IStreamGeometryImpl Clone()
{ {
get return this;
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
} }
public IStreamGeometryImpl Clone() public void Dispose()
{ {
return this;
} }
public bool FillContains(Point point) public bool FillContains(Point point)
@ -88,7 +79,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
return _impl.FillContains(point); return _impl.FillContains(point);
} }
public Rect GetRenderBounds(double strokeThickness) public Rect GetRenderBounds(Pen pen)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -108,7 +99,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IGeometryImpl WithTransform(Matrix transform) public ITransformedGeometryImpl WithTransform(Matrix transform)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

Loading…
Cancel
Save