diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs
index f425ae3f01..002c5ea3f2 100644
--- a/src/Avalonia.Controls/Border.cs
+++ b/src/Avalonia.Controls/Border.cs
@@ -108,18 +108,7 @@ namespace Avalonia.Controls
/// The desired size of the control.
protected override Size MeasureOverride(Size availableSize)
{
- var child = Child;
- 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);
- }
+ return MeasureOverrideImpl(availableSize, Child, Padding, BorderThickness);
}
///
@@ -129,15 +118,32 @@ namespace Avalonia.Controls
/// The space taken.
protected override Size ArrangeOverride(Size finalSize)
{
- var child = Child;
-
- if (child != null)
+ if (Child != null)
{
var padding = Padding + new Thickness(BorderThickness);
- child.Arrange(new Rect(finalSize).Deflate(padding));
+ Child.Arrange(new Rect(finalSize).Deflate(padding));
}
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);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index a97fdf8784..d0a438cc2b 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/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.
using System;
-using System.Reactive.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Metadata;
-using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters
{
@@ -340,94 +338,121 @@ namespace Avalonia.Controls.Presenters
///
protected override Size MeasureOverride(Size availableSize)
{
- var child = Child;
- var padding = Padding + new Thickness(BorderThickness);
+ return Border.MeasureOverrideImpl(availableSize, Child, Padding, BorderThickness);
+ }
+
+ ///
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ return ArrangeOverrideImpl(finalSize, new Vector());
+ }
+
+ ///
+ /// Called when the property changes.
+ ///
+ /// The event args.
+ private void ContentChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ _createdChild = false;
- if (child != null)
+ if (((ILogical)this).IsAttachedToLogicalTree)
{
- child.Measure(availableSize.Deflate(padding));
- return child.DesiredSize.Inflate(padding);
+ UpdateChild();
}
- 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();
}
- ///
- protected override Size ArrangeOverride(Size finalSize)
+ internal Size ArrangeOverrideImpl(Size finalSize, Vector offset)
{
- var child = Child;
-
- if (child != null)
+ if (Child != null)
{
- var padding = Padding + new Thickness(BorderThickness);
- var sizeMinusPadding = finalSize.Deflate(padding);
- var size = sizeMinusPadding;
- var horizontalAlignment = HorizontalContentAlignment;
- var verticalAlignment = VerticalContentAlignment;
- var originX = padding.Left;
- var originY = padding.Top;
-
- if (horizontalAlignment != HorizontalAlignment.Stretch)
+ var padding = Padding;
+ var borderThickness = BorderThickness;
+ var horizontalContentAlignment = HorizontalContentAlignment;
+ var verticalContentAlignment = VerticalContentAlignment;
+ var useLayoutRounding = UseLayoutRounding;
+ var availableSizeMinusMargins = new Size(
+ Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness),
+ Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness));
+ 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:
- originX += (sizeMinusPadding.Width - size.Width) / 2;
+ case HorizontalAlignment.Stretch:
+ originX += (availableSizeMinusMargins.Width - size.Width) / 2;
break;
case HorizontalAlignment.Right:
- originX = size.Width - child.DesiredSize.Width;
+ originX += availableSizeMinusMargins.Width - size.Width;
break;
}
- switch (verticalAlignment)
+ switch (verticalContentAlignment)
{
- case VerticalAlignment.Stretch:
case VerticalAlignment.Center:
- originY += (sizeMinusPadding.Height - size.Height) / 2;
+ case VerticalAlignment.Stretch:
+ originY += (availableSizeMinusMargins.Height - size.Height) / 2;
break;
case VerticalAlignment.Bottom:
- originY = size.Height - child.DesiredSize.Height;
+ originY += availableSizeMinusMargins.Height - size.Height;
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;
}
- ///
- /// Called when the property changes.
- ///
- /// The event args.
- private void ContentChanged(AvaloniaPropertyChangedEventArgs e)
+ private double GetLayoutScale()
{
- _createdChild = false;
+ var result = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0;
- if (((ILogical)this).IsAttachedToLogicalTree)
+ if (result == 0 || double.IsNaN(result) || double.IsInfinity(result))
{
- UpdateChild();
- }
- else if (Child != null)
- {
- VisualChildren.Remove(Child);
- LogicalChildren.Remove(Child);
- Child = null;
- _dataTemplate = null;
+ throw new Exception($"Invalid LayoutScaling returned from {VisualRoot.GetType()}");
}
- InvalidateMeasure();
+ return result;
}
private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e)
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 6c61375054..a68979cfa1 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -62,7 +62,6 @@ namespace Avalonia.Controls.Presenters
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private Size _extent;
- private Size _measuredExtent;
private Vector _offset;
private IDisposable _logicalScrollSubscription;
private Size _viewport;
@@ -199,65 +198,34 @@ namespace Avalonia.Controls.Presenters
///
protected override Size MeasureOverride(Size availableSize)
{
- var child = Child;
-
- if (child != null)
+ if (_logicalScrollSubscription != null || Child == null)
{
- var measureSize = availableSize;
-
- if (_logicalScrollSubscription == null)
- {
- measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
-
- if (!CanHorizontallyScroll)
- {
- measureSize = measureSize.WithWidth(availableSize.Width);
- }
+ return base.MeasureOverride(availableSize);
+ }
- if (!CanVerticallyScroll)
- {
- measureSize = measureSize.WithHeight(availableSize.Height);
- }
- }
+ var constraint = new Size(
+ CanHorizontallyScroll ? double.PositiveInfinity : availableSize.Width,
+ CanVerticallyScroll ? double.PositiveInfinity : availableSize.Height);
- child.Measure(measureSize);
- var size = child.DesiredSize;
- _measuredExtent = size;
- return size.Constrain(availableSize);
- }
- else
- {
- return Extent = new Size();
- }
+ Child.Measure(constraint);
+ return Child.DesiredSize.Constrain(availableSize);
}
///
protected override Size ArrangeOverride(Size finalSize)
{
- var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable;
- 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)
+ if (_logicalScrollSubscription != null || Child == null)
{
- child.Arrange(new Rect(finalSize));
- return finalSize;
+ return base.ArrangeOverride(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;
}
///
diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs
index a469f09867..51c22c88e7 100644
--- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs
+++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs
@@ -53,12 +53,13 @@ namespace Avalonia.Controls.Primitives
foreach (var child in Children)
{
- var info = (AdornedElementInfo)child.GetValue(s_adornedElementInfoProperty);
+ var info = child.GetValue(s_adornedElementInfoProperty);
if (info != null && info.Bounds.HasValue)
{
child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform);
child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute);
+ UpdateClip(child, info.Bounds.Value);
child.Arrange(info.Bounds.Value.Bounds);
}
else
@@ -78,6 +79,19 @@ namespace Avalonia.Controls.Primitives
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)
{
switch (e.Action)
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 8f4606884e..8b37689591 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -325,7 +325,7 @@ namespace Avalonia.Controls
string text = Text ?? string.Empty;
int caretIndex = CaretIndex;
bool movement = false;
- bool handled = true;
+ bool handled = false;
var modifiers = e.Modifiers;
switch (e.Key)
@@ -334,13 +334,14 @@ namespace Avalonia.Controls
if (modifiers == InputModifiers.Control)
{
SelectAll();
+ handled = true;
}
-
break;
case Key.C:
if (modifiers == InputModifiers.Control)
{
Copy();
+ handled = true;
}
break;
@@ -349,6 +350,7 @@ namespace Avalonia.Controls
{
Copy();
DeleteSelection();
+ handled = true;
}
break;
@@ -356,19 +358,24 @@ namespace Avalonia.Controls
if (modifiers == InputModifiers.Control)
{
Paste();
+ handled = true;
}
break;
case Key.Z:
if (modifiers == InputModifiers.Control)
+ {
_undoRedoHelper.Undo();
-
+ handled = true;
+ }
break;
case Key.Y:
if (modifiers == InputModifiers.Control)
+ {
_undoRedoHelper.Redo();
-
+ handled = true;
+ }
break;
case Key.Left:
MoveHorizontal(-1, modifiers);
@@ -381,13 +388,11 @@ namespace Avalonia.Controls
break;
case Key.Up:
- MoveVertical(-1, modifiers);
- movement = true;
+ movement = MoveVertical(-1, modifiers);
break;
case Key.Down:
- MoveVertical(1, modifiers);
- movement = true;
+ movement = MoveVertical(1, modifiers);
break;
case Key.Home:
@@ -423,7 +428,7 @@ namespace Avalonia.Controls
CaretIndex -= removedCharacters;
SelectionStart = SelectionEnd = CaretIndex;
}
-
+ handled = true;
break;
case Key.Delete:
@@ -447,13 +452,14 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters));
}
-
+ handled = true;
break;
case Key.Enter:
if (AcceptsReturn)
{
HandleTextInput("\r\n");
+ handled = true;
}
break;
@@ -462,11 +468,11 @@ namespace Avalonia.Controls
if (AcceptsTab)
{
HandleTextInput("\t");
+ handled = true;
}
else
{
base.OnKeyDown(e);
- handled = false;
}
break;
@@ -485,7 +491,7 @@ namespace Avalonia.Controls
SelectionStart = SelectionEnd = CaretIndex;
}
- if (handled)
+ if (handled || movement)
{
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 lines = formattedText.GetLines().ToList();
@@ -677,6 +683,11 @@ namespace Avalonia.Controls
var point = new Point(rect.X, y + (count * (line.Height / 2)));
var hit = formattedText.HitTestPoint(point);
CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0);
+ return true;
+ }
+ else
+ {
+ return false;
}
}
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index 64912f20df..7fed712e07 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -373,28 +373,28 @@ namespace Avalonia.Controls
protected override Size MeasureOverride(Size availableSize)
{
var sizeToContent = SizeToContent;
- var size = ClientSize;
- var desired = base.MeasureOverride(availableSize.Constrain(_maxPlatformClientSize));
+ var clientSize = ClientSize;
+ Size constraint;
switch (sizeToContent)
{
case SizeToContent.Width:
- size = new Size(desired.Width, ClientSize.Height);
+ constraint = new Size(double.PositiveInfinity, ClientSize.Height);
break;
case SizeToContent.Height:
- size = new Size(ClientSize.Width, desired.Height);
+ constraint = new Size(ClientSize.Width, double.PositiveInfinity);
break;
case SizeToContent.WidthAndHeight:
- size = new Size(desired.Width, desired.Height);
+ constraint = Size.Infinity;
break;
case SizeToContent.Manual:
- size = ClientSize;
+ constraint = ClientSize;
break;
default:
throw new InvalidOperationException("Invalid value for SizeToContent.");
}
- return size;
+ return base.MeasureOverride(constraint);
}
protected override void HandleClosed()
diff --git a/src/Avalonia.Visuals/Media/EllipseGeometry.cs b/src/Avalonia.Visuals/Media/EllipseGeometry.cs
index 591b55cf58..ca84d4cc7b 100644
--- a/src/Avalonia.Visuals/Media/EllipseGeometry.cs
+++ b/src/Avalonia.Visuals/Media/EllipseGeometry.cs
@@ -17,15 +17,9 @@ namespace Avalonia.Media
public static readonly StyledProperty RectProperty =
AvaloniaProperty.Register(nameof(Rect));
- public Rect Rect
- {
- get => GetValue(RectProperty);
- set => SetValue(RectProperty, value);
- }
-
static EllipseGeometry()
{
- RectProperty.Changed.AddClassHandler(x => x.RectChanged);
+ AffectsGeometry(RectProperty);
}
///
@@ -33,8 +27,6 @@ namespace Avalonia.Media
///
public EllipseGeometry()
{
- IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService();
- PlatformImpl = factory.CreateStreamGeometry();
}
///
@@ -46,17 +38,30 @@ namespace Avalonia.Media
Rect = rect;
}
+ ///
+ /// Gets or sets a rect that defines the bounds of the ellipse.
+ ///
+ public Rect Rect
+ {
+ get => GetValue(RectProperty);
+ set => SetValue(RectProperty, value);
+ }
+
///
public override Geometry Clone()
{
return new EllipseGeometry(Rect);
}
- private void RectChanged(AvaloniaPropertyChangedEventArgs e)
+ ///
+ protected override IGeometryImpl CreateDefiningGeometry()
{
- var rect = (Rect)e.NewValue;
- using (var ctx = ((IStreamGeometryImpl)PlatformImpl).Open())
+ var factory = AvaloniaLocator.Current.GetService();
+ var geometry = factory.CreateStreamGeometry();
+
+ using (var ctx = geometry.Open())
{
+ var rect = Rect;
double controlPointRatio = (Math.Sqrt(2) - 1) * 4 / 3;
var center = rect.Center;
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.EndFigure(true);
}
+
+ return geometry;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/Geometry.cs b/src/Avalonia.Visuals/Media/Geometry.cs
index d27626bcc1..17c0d2ab74 100644
--- a/src/Avalonia.Visuals/Media/Geometry.cs
+++ b/src/Avalonia.Visuals/Media/Geometry.cs
@@ -17,26 +17,47 @@ namespace Avalonia.Media
public static readonly StyledProperty TransformProperty =
AvaloniaProperty.Register(nameof(Transform));
- ///
- /// Initializes static members of the class.
- ///
+ private bool _isDirty = true;
+ private IGeometryImpl _platformImpl;
+
static Geometry()
{
TransformProperty.Changed.AddClassHandler(x => x.TransformChanged);
}
+ ///
+ /// Raised when the geometry changes.
+ ///
+ public event EventHandler Changed;
+
///
/// Gets the geometry's bounding rectangle.
///
- public Rect Bounds => PlatformImpl.Bounds;
+ public Rect Bounds => PlatformImpl?.Bounds ?? Rect.Empty;
///
/// Gets the platform-specific implementation of the geometry.
///
- public virtual IGeometryImpl PlatformImpl
+ public IGeometryImpl PlatformImpl
{
- get;
- protected set;
+ get
+ {
+ 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;
+ }
}
///
@@ -55,14 +76,11 @@ namespace Avalonia.Media
public abstract Geometry Clone();
///
- /// Gets the geometry's bounding rectangle with the specified stroke thickness.
+ /// Gets the geometry's bounding rectangle with the specified pen.
///
- /// The stroke thickness.
+ /// The stroke thickness.
/// The bounding rectangle.
- public Rect GetRenderBounds(double strokeThickness)
- {
- return PlatformImpl.GetRenderBounds(strokeThickness);
- }
+ public Rect GetRenderBounds(Pen pen) => PlatformImpl?.GetRenderBounds(pen) ?? Rect.Empty;
///
/// Indicates whether the geometry's fill contains the specified point.
@@ -71,7 +89,7 @@ namespace Avalonia.Media
/// true if the geometry contains the point; otherwise, false.
public bool FillContains(Point point)
{
- return PlatformImpl.FillContains(point);
+ return PlatformImpl?.FillContains(point) == true;
}
///
@@ -82,13 +100,86 @@ namespace Avalonia.Media
/// true if the geometry contains the point; otherwise, false.
public bool StrokeContains(Pen pen, Point point)
{
- return PlatformImpl.StrokeContains(pen, point);
+ return PlatformImpl?.StrokeContains(pen, point) == true;
+ }
+
+ ///
+ /// Marks a property as affecting the geometry's .
+ ///
+ /// The properties.
+ ///
+ /// After a call to this method in a control's static constructor, any change to the
+ /// property will cause to be called on the element.
+ ///
+ protected static void AffectsGeometry(params AvaloniaProperty[] properties)
+ {
+ foreach (var property in properties)
+ {
+ property.Changed.Subscribe(AffectsGeometryInvalidate);
+ }
+ }
+
+ ///
+ /// Creates the platform implementation of the geometry, without the transform applied.
+ ///
+ ///
+ protected abstract IGeometryImpl CreateDefiningGeometry();
+
+ ///
+ /// Invalidates the platform implementation of the geometry.
+ ///
+ protected void InvalidateGeometry()
+ {
+ _isDirty = true;
+ _platformImpl = null;
+ Changed?.Invoke(this, EventArgs.Empty);
}
private void TransformChanged(AvaloniaPropertyChangedEventArgs e)
{
- var transform = (Transform)e.NewValue;
- PlatformImpl = PlatformImpl.WithTransform(transform.Value);
+ var oldValue = (Transform)e.OldValue;
+ 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();
}
}
}
diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs
index e67e853a84..a26a5341c8 100644
--- a/src/Avalonia.Visuals/Media/GeometryDrawing.cs
+++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs
@@ -37,7 +37,8 @@
public override Rect GetBounds()
{
// 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();
}
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Visuals/Media/LineGeometry.cs b/src/Avalonia.Visuals/Media/LineGeometry.cs
index 0952d9644f..f7ba4ccb0e 100644
--- a/src/Avalonia.Visuals/Media/LineGeometry.cs
+++ b/src/Avalonia.Visuals/Media/LineGeometry.cs
@@ -16,29 +16,15 @@ namespace Avalonia.Media
public static readonly StyledProperty StartPointProperty =
AvaloniaProperty.Register(nameof(StartPoint));
- public Point StartPoint
- {
- get => GetValue(StartPointProperty);
- set => SetValue(StartPointProperty, value);
- }
-
///
/// Defines the property.
///
public static readonly StyledProperty EndPointProperty =
AvaloniaProperty.Register(nameof(EndPoint));
- private bool _isDirty = true;
-
- public Point EndPoint
- {
- get => GetValue(EndPointProperty);
- set => SetValue(EndPointProperty, value);
- }
static LineGeometry()
{
- StartPointProperty.Changed.AddClassHandler(x => x.PointsChanged);
- EndPointProperty.Changed.AddClassHandler(x => x.PointsChanged);
+ AffectsGeometry(StartPointProperty, EndPointProperty);
}
///
@@ -46,8 +32,6 @@ namespace Avalonia.Media
///
public LineGeometry()
{
- IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService();
- PlatformImpl = factory.CreateStreamGeometry();
}
///
@@ -61,38 +45,44 @@ namespace Avalonia.Media
EndPoint = endPoint;
}
- public override IGeometryImpl PlatformImpl
+ ///
+ /// Gets or sets the start point of the line.
+ ///
+ public Point StartPoint
{
- get
- {
- PrepareIfNeeded();
- return base.PlatformImpl;
- }
- protected set => base.PlatformImpl = value;
+ get => GetValue(StartPointProperty);
+ set => SetValue(StartPointProperty, value);
}
- public void PrepareIfNeeded()
+ ///
+ /// Gets or sets the end point of the line.
+ ///
+ public Point EndPoint
{
- if (_isDirty)
- {
- _isDirty = false;
-
- using (var context = ((IStreamGeometryImpl)PlatformImpl).Open())
- {
- context.BeginFigure(StartPoint, false);
- context.LineTo(EndPoint);
- context.EndFigure(false);
- }
- }
+ get => GetValue(EndPointProperty);
+ set => SetValue(EndPointProperty, value);
}
///
public override Geometry Clone()
{
- PrepareIfNeeded();
return new LineGeometry(StartPoint, EndPoint);
}
- private void PointsChanged(AvaloniaPropertyChangedEventArgs e) => _isDirty = true;
+ ///
+ protected override IGeometryImpl CreateDefiningGeometry()
+ {
+ var factory = AvaloniaLocator.Current.GetService();
+ var geometry = factory.CreateStreamGeometry();
+
+ using (var context = geometry.Open())
+ {
+ context.BeginFigure(StartPoint, false);
+ context.LineTo(EndPoint);
+ context.EndFigure(false);
+ }
+
+ return geometry;
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/PathGeometry.cs b/src/Avalonia.Visuals/Media/PathGeometry.cs
index df3dd47c8a..ecda07ada1 100644
--- a/src/Avalonia.Visuals/Media/PathGeometry.cs
+++ b/src/Avalonia.Visuals/Media/PathGeometry.cs
@@ -1,10 +1,10 @@
// 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.Collections;
using Avalonia.Metadata;
using Avalonia.Platform;
-using System;
namespace Avalonia.Media
{
@@ -22,12 +22,14 @@ namespace Avalonia.Media
public static readonly StyledProperty FillRuleProperty =
AvaloniaProperty.Register(nameof(FillRule));
+ private PathFigures _figures;
+ private IDisposable _figuresObserver;
+ private IDisposable _figuresPropertiesObserver;
+
static PathGeometry()
{
- FiguresProperty.Changed.Subscribe(onNext: v =>
- {
- (v.Sender as PathGeometry)?.OnFiguresChanged(v.OldValue as PathFigures, v.NewValue as PathFigures);
- });
+ FiguresProperty.Changed.AddClassHandler((s, e) =>
+ s.OnFiguresChanged(e.NewValue as PathFigures));
}
///
@@ -63,61 +65,33 @@ namespace Avalonia.Media
set { SetValue(FillRuleProperty, value); }
}
- public override IGeometryImpl PlatformImpl
+ protected override IGeometryImpl CreateDefiningGeometry()
{
- get
- {
- PrepareIfNeeded();
- return base.PlatformImpl;
- }
+ var factory = AvaloniaLocator.Current.GetService();
+ var geometry = factory.CreateStreamGeometry();
- protected set
+ using (var ctx = new StreamGeometryContext(geometry.Open()))
{
- base.PlatformImpl = value;
- }
- }
-
- public override Geometry Clone()
- {
- PrepareIfNeeded();
-
- return base.Clone();
- }
-
- public void PrepareIfNeeded()
- {
- if (_isDirty)
- {
- _isDirty = false;
-
- using (var ctx = Open())
+ ctx.SetFillRule(FillRule);
+ foreach (var f in Figures)
{
- ctx.SetFillRule(FillRule);
- foreach (var f in Figures)
- {
- f.ApplyTo(ctx);
- }
+ f.ApplyTo(ctx);
}
}
- }
- internal void NotifyChanged()
- {
- _isDirty = true;
+ return geometry;
}
- private PathFigures _figures;
- private IDisposable _figuresObserver = null;
- private IDisposable _figuresPropertiesObserver = null;
- private bool _isDirty = true;
-
- private void OnFiguresChanged(PathFigures oldValue, PathFigures newValue)
+ private void OnFiguresChanged(PathFigures figures)
{
_figuresObserver?.Dispose();
_figuresPropertiesObserver?.Dispose();
- _figuresObserver = newValue?.ForEachItem(f => NotifyChanged(), f => NotifyChanged(), () => NotifyChanged());
- _figuresPropertiesObserver = newValue?.TrackItemPropertyChanged(t => NotifyChanged());
+ _figuresObserver = figures?.ForEachItem(
+ _ => InvalidateGeometry(),
+ _ => InvalidateGeometry(),
+ () => InvalidateGeometry());
+ _figuresPropertiesObserver = figures?.TrackItemPropertyChanged(_ => InvalidateGeometry());
}
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Visuals/Media/PolylineGeometry.cs b/src/Avalonia.Visuals/Media/PolylineGeometry.cs
index b23bb88729..06dbcccf3e 100644
--- a/src/Avalonia.Visuals/Media/PolylineGeometry.cs
+++ b/src/Avalonia.Visuals/Media/PolylineGeometry.cs
@@ -27,14 +27,12 @@ namespace Avalonia.Media
AvaloniaProperty.Register(nameof(IsFilled));
private Points _points;
- private bool _isDirty = true;
private IDisposable _pointsObserver;
static PolylineGeometry()
{
- PointsProperty.Changed.AddClassHandler((s, e) =>
- s.OnPointsChanged(e.OldValue as Points, e.NewValue as Points));
- IsFilledProperty.Changed.AddClassHandler((s, _) => s.NotifyChanged());
+ AffectsGeometry(IsFilledProperty);
+ PointsProperty.Changed.AddClassHandler((s, e) => s.OnPointsChanged(e.NewValue as Points));
}
///
@@ -42,9 +40,6 @@ namespace Avalonia.Media
///
public PolylineGeometry()
{
- IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService();
- PlatformImpl = factory.CreateStreamGeometry();
-
Points = new Points();
}
@@ -57,29 +52,6 @@ namespace Avalonia.Media
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);
- }
- }
- }
- }
-
///
/// Gets or sets the figures.
///
@@ -99,33 +71,42 @@ namespace Avalonia.Media
set => SetValue(IsFilledProperty, value);
}
- public override IGeometryImpl PlatformImpl
- {
- get
- {
- PrepareIfNeeded();
- return base.PlatformImpl;
- }
- protected set => base.PlatformImpl = value;
- }
-
///
public override Geometry Clone()
{
- PrepareIfNeeded();
return new PolylineGeometry(Points, IsFilled);
}
- private void OnPointsChanged(Points oldValue, Points newValue)
+ protected override IGeometryImpl CreateDefiningGeometry()
{
- _pointsObserver?.Dispose();
+ var factory = AvaloniaLocator.Current.GetService();
+ 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);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/RectangleGeometry.cs b/src/Avalonia.Visuals/Media/RectangleGeometry.cs
index 1aa449d9e1..3ccfd80f93 100644
--- a/src/Avalonia.Visuals/Media/RectangleGeometry.cs
+++ b/src/Avalonia.Visuals/Media/RectangleGeometry.cs
@@ -24,7 +24,7 @@ namespace Avalonia.Media
static RectangleGeometry()
{
- RectProperty.Changed.AddClassHandler(x => x.RectChanged);
+ AffectsGeometry(RectProperty);
}
///
@@ -32,36 +32,36 @@ namespace Avalonia.Media
///
public RectangleGeometry()
{
- IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService();
- PlatformImpl = factory.CreateStreamGeometry();
}
///
/// Initializes a new instance of the class.
///
/// The rectangle bounds.
- public RectangleGeometry(Rect rect) : this()
+ public RectangleGeometry(Rect rect)
{
Rect = rect;
}
///
- public override Geometry Clone()
- {
- return new RectangleGeometry(Rect);
- }
+ public override Geometry Clone() => new RectangleGeometry(Rect);
- private void RectChanged(AvaloniaPropertyChangedEventArgs e)
+ protected override IGeometryImpl CreateDefiningGeometry()
{
- var rect = (Rect)e.NewValue;
- using (var context = ((IStreamGeometryImpl)PlatformImpl).Open())
+ var factory = AvaloniaLocator.Current.GetService();
+ var geometry = factory.CreateStreamGeometry();
+
+ using (var context = geometry.Open())
{
+ var rect = Rect;
context.BeginFigure(rect.TopLeft, true);
context.LineTo(rect.TopRight);
context.LineTo(rect.BottomRight);
context.LineTo(rect.BottomLeft);
context.EndFigure(true);
}
+
+ return geometry;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs
index a1f62b172d..9c29c62bad 100644
--- a/src/Avalonia.Visuals/Media/StreamGeometry.cs
+++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs
@@ -10,22 +10,22 @@ namespace Avalonia.Media
///
public class StreamGeometry : Geometry
{
+ IStreamGeometryImpl _impl;
+
///
/// Initializes a new instance of the class.
///
public StreamGeometry()
{
- IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService();
- PlatformImpl = factory.CreateStreamGeometry();
}
///
/// Initializes a new instance of the class.
///
/// The platform-specific implementation.
- private StreamGeometry(IGeometryImpl impl)
+ private StreamGeometry(IStreamGeometryImpl impl)
{
- PlatformImpl = impl;
+ _impl = impl;
}
///
@@ -61,5 +61,17 @@ namespace Avalonia.Media
{
return new StreamGeometryContext(((IStreamGeometryImpl)PlatformImpl).Open());
}
+
+ ///
+ protected override IGeometryImpl CreateDefiningGeometry()
+ {
+ if (_impl == null)
+ {
+ var factory = AvaloniaLocator.Current.GetService();
+ _impl = factory.CreateStreamGeometry();
+ }
+
+ return _impl;
+ }
}
}
diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs
index 132e00e56b..d93bdc0c20 100644
--- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs
@@ -1,12 +1,13 @@
// 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.Media;
namespace Avalonia.Platform
{
///
- /// Defines the platform-specific interface for .
+ /// Defines the platform-specific interface for a .
///
public interface IGeometryImpl
{
@@ -16,16 +17,11 @@ namespace Avalonia.Platform
Rect Bounds { get; }
///
- /// Gets the transform to applied to the geometry.
+ /// Gets the geometry's bounding rectangle with the specified pen.
///
- Matrix Transform { get; }
-
- ///
- /// Gets the geometry's bounding rectangle with the specified stroke thickness.
- ///
- /// The stroke thickness.
+ /// The pen to use. May be null.
/// The bounding rectangle.
- Rect GetRenderBounds(double strokeThickness);
+ Rect GetRenderBounds(Pen pen);
///
/// Indicates whether the geometry's fill contains the specified point.
@@ -54,6 +50,6 @@ namespace Avalonia.Platform
///
/// The transform.
/// The cloned geometry.
- IGeometryImpl WithTransform(Matrix transform);
+ ITransformedGeometryImpl WithTransform(Matrix transform);
}
}
diff --git a/src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs b/src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs
new file mode 100644
index 0000000000..ca68005906
--- /dev/null
+++ b/src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Avalonia.Platform
+{
+ ///
+ /// Represents a geometry with a transform applied.
+ ///
+ ///
+ /// An transforms a geometry without transforming its
+ /// stroke thickness.
+ ///
+ public interface ITransformedGeometryImpl : IGeometryImpl
+ {
+ ///
+ /// Gets the source geometry that the is applied to.
+ ///
+ IGeometryImpl SourceGeometry { get; }
+
+ ///
+ /// Gets the applied transform.
+ ///
+ Matrix Transform { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
index 6310122183..7b79ebab4f 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
@@ -28,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph
Pen pen,
IGeometryImpl geometry,
IDictionary childScenes = null)
- : base(geometry.GetRenderBounds(pen?.Thickness ?? 0), transform, null)
+ : base(geometry.GetRenderBounds(pen), transform, null)
{
Transform = transform;
Brush = brush?.ToImmutable();
diff --git a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs
index 4c548669bd..435ca85a05 100644
--- a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs
+++ b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs
@@ -24,17 +24,17 @@ namespace Avalonia.VisualTree
}
///
- /// Gets the control's bounds.
+ /// Gets the control's bounds in its local coordinate space.
///
public Rect Bounds { get; }
///
- /// Gets the control's clip rectangle.
+ /// Gets the control's clip rectangle in global coordinate space.
///
public Rect Clip { get; }
///
- /// Gets the control's transform.
+ /// Gets the transform from local to global coordinate space.
///
public Matrix Transform { get; }
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index 0bc133e9df..22e5652cfb 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -69,7 +69,7 @@ namespace Avalonia.Skia
public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
{
- var impl = (StreamGeometryImpl)geometry;
+ var impl = (GeometryImpl)geometry;
var size = geometry.Bounds.Size;
using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))
diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs
new file mode 100644
index 0000000000..fb134b728c
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
index 9a0a1dc434..935d6d5e5b 100644
--- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
@@ -1,43 +1,30 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Text;
using Avalonia.Media;
using Avalonia.Platform;
-using Avalonia.RenderHelpers;
using SkiaSharp;
namespace Avalonia.Skia
{
- class StreamGeometryImpl : IStreamGeometryImpl
+ class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl
{
+ Rect _bounds;
SKPath _path;
- private Matrix _transform = Matrix.Identity;
+ public override SKPath EffectivePath => _path;
- public SKPath EffectivePath => _path;
-
- public Rect GetRenderBounds(double strokeThickness)
+ public override Rect GetRenderBounds(Pen pen)
{
- // TODO: Calculate properly.
- return Bounds.TransformToAABB(Transform).Inflate(strokeThickness);
+ return GetRenderBounds(pen?.Thickness ?? 0);
}
- public Rect Bounds { get; private set; }
-
- public Matrix Transform
- {
- get { return _transform; }
- }
+ public override Rect Bounds => _bounds;
public IStreamGeometryImpl Clone()
{
return new StreamGeometryImpl
{
_path = _path?.Clone(),
- _transform = Transform,
- Bounds = Bounds
+ _bounds = Bounds
};
}
@@ -49,41 +36,34 @@ namespace Avalonia.Skia
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
// return EffectivePath.Contains(point.X, point.Y);
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
// return EffectivePath.Contains(point.X, point.Y);
return GetRenderBounds(0).Contains(point);
}
- public IGeometryImpl Intersect(IGeometryImpl geometry)
+ public override IGeometryImpl Intersect(IGeometryImpl geometry)
{
throw new NotImplementedException();
}
- public IGeometryImpl WithTransform(Matrix transform)
+ public override ITransformedGeometryImpl WithTransform(Matrix transform)
{
- var result = (StreamGeometryImpl)Clone();
-
- if (result.Transform != Matrix.Identity)
- {
- result._path.Transform(result.Transform.Invert().ToSKMatrix());
- }
-
- if (transform != Matrix.Identity)
- {
- result._path.Transform(transform.ToSKMatrix());
- }
+ return new TransformedGeometryImpl(this, transform);
+ }
- result._transform = transform;
- return result;
+ private Rect GetRenderBounds(double strokeThickness)
+ {
+ // TODO: Calculate properly.
+ return Bounds.Inflate(strokeThickness);
}
class StreamContext : IStreamGeometryContextImpl
@@ -102,7 +82,7 @@ namespace Avalonia.Skia
{
SKRect 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)
diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs
new file mode 100644
index 0000000000..e14d3f04be
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
index ec21741100..bdbbdab2b9 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
@@ -189,7 +189,7 @@ namespace Avalonia.Direct2D1.Media
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))
{
if (d2dBrush.PlatformBrush != null)
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
index 8bb901a1e4..8f11d1463b 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
@@ -24,12 +24,10 @@ namespace Avalonia.Direct2D1.Media
public Geometry Geometry { get; }
///
- public virtual Matrix Transform => Matrix.Identity;
-
- ///
- public Rect GetRenderBounds(double strokeThickness)
+ public Rect GetRenderBounds(Avalonia.Media.Pen pen)
{
- return Geometry.GetWidenedBounds((float)strokeThickness).ToAvalonia();
+ var factory = AvaloniaLocator.Current.GetService();
+ return Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia();
}
///
@@ -56,15 +54,15 @@ namespace Avalonia.Direct2D1.Media
return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness);
}
- ///
- public IGeometryImpl WithTransform(Matrix transform)
+ public ITransformedGeometryImpl WithTransform(Matrix transform)
{
var factory = AvaloniaLocator.Current.GetService();
return new TransformedGeometryImpl(
new TransformedGeometry(
factory,
GetSourceGeometry(),
- transform.ToDirect2D()));
+ transform.ToDirect2D()),
+ this);
}
protected virtual Geometry GetSourceGeometry() => Geometry;
diff --git a/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs
index 4043e180dc..e0e9e340bb 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs
@@ -1,23 +1,27 @@
// 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.Platform;
using SharpDX.Direct2D1;
namespace Avalonia.Direct2D1.Media
{
- public class TransformedGeometryImpl : GeometryImpl
+ public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl
{
///
/// Initializes a new instance of the class.
///
/// An existing Direct2D .
- public TransformedGeometryImpl(TransformedGeometry geometry)
+ public TransformedGeometryImpl(TransformedGeometry geometry, GeometryImpl source)
: base(geometry)
{
+ SourceGeometry = source;
}
+ public IGeometryImpl SourceGeometry { get; }
+
///
- public override Matrix Transform => ((TransformedGeometry)Geometry).Transform.ToAvalonia();
+ public Matrix Transform => ((TransformedGeometry)Geometry).Transform.ToAvalonia();
protected override Geometry GetSourceGeometry() => ((TransformedGeometry)Geometry).SourceGeometry;
}
diff --git a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
index 118b6deb97..c76a5b5da5 100644
--- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
+++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
@@ -105,7 +105,18 @@ namespace Avalonia.Direct2D1
/// The pen to convert.
/// The render target.
/// The Direct2D brush.
- 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);
+ }
+
+ ///
+ /// Converts a pen to a Direct2D stroke style.
+ ///
+ /// The pen to convert.
+ /// The render target.
+ /// The Direct2D brush.
+ public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, Factory factory)
{
var properties = new StrokeStyleProperties
{
@@ -123,7 +134,7 @@ namespace Avalonia.Direct2D1
properties.DashOffset = (float)pen.DashStyle.Offset;
dashes = pen.DashStyle?.Dashes.Select(x => (float)x).ToArray();
}
- return new StrokeStyle(target.Factory, properties, dashes);
+ return new StrokeStyle(factory, properties, dashes);
}
///
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs
new file mode 100644
index 0000000000..450b85696e
--- /dev/null
+++ b/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()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs
index f1529b5090..7eb44c5354 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs
+++ b/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));
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);
target.VirtualizationMode = ItemVirtualizationMode.Simple;
@@ -266,7 +266,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
scroll.Arrange(new Rect(0, 0, 100, 100));
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);
}
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
index ebed83e99a..3c8a692bfb 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
@@ -13,55 +13,32 @@ namespace Avalonia.Controls.UnitTests.Presenters
{
public class ScrollContentPresenterTests
{
- [Fact]
- public void Content_Can_Be_Left_Aligned()
- {
- Border content;
- var target = new ScrollContentPresenter
- {
- Content = content = new Border
- {
- Padding = new Thickness(8),
- HorizontalAlignment = HorizontalAlignment.Left
- },
- };
-
- target.UpdateChild();
- target.Measure(new Size(100, 100));
- 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()
+ [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 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 ScrollContentPresenter
{
+ Padding = new Thickness(10),
Content = content = new Border
{
- Padding = new Thickness(8),
- HorizontalAlignment = HorizontalAlignment.Right
+ MinWidth = 16,
+ MinHeight = 16,
+ HorizontalAlignment = h,
+ VerticalAlignment = v,
},
};
@@ -69,19 +46,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(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]
- public void Content_Can_Be_Bottom_Aligned()
+ public void DesiredSize_Is_Content_Size_When_Smaller_Than_AvailableSize()
{
- Border content;
var target = new ScrollContentPresenter
{
- Content = content = new Border
+ Padding = new Thickness(10),
+ Content = new Border
{
- Padding = new Thickness(8),
- VerticalAlignment = VerticalAlignment.Bottom,
+ MinWidth = 16,
+ MinHeight = 16,
},
};
@@ -89,20 +66,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(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]
- public void Content_Can_Be_TopRight_Aligned()
+ public void DesiredSize_Is_AvailableSize_When_Content_Larger_Than_AvailableSize()
{
- Border content;
var target = new ScrollContentPresenter
{
- Content = content = new Border
+ Padding = new Thickness(10),
+ Content = new Border
{
- Padding = new Thickness(8),
- HorizontalAlignment = HorizontalAlignment.Right,
- VerticalAlignment = VerticalAlignment.Top,
+ MinWidth = 160,
+ MinHeight = 160,
},
};
@@ -110,7 +86,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(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]
@@ -208,6 +184,71 @@ namespace Avalonia.Controls.UnitTests.Presenters
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]
public void Setting_Offset_Should_Invalidate_Arrange()
{
diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/GeometryTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/GeometryTests.cs
index 4b8933b0f8..963c75078b 100644
--- a/tests/Avalonia.Direct2D1.UnitTests/Media/GeometryTests.cs
+++ b/tests/Avalonia.Direct2D1.UnitTests/Media/GeometryTests.cs
@@ -31,8 +31,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
Direct2D1Platform.Initialize();
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);
}
}
}
diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs
index dcc65edc74..410b2ffb2e 100644
--- a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs
+++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs
@@ -7,6 +7,97 @@ namespace Avalonia.Layout.UnitTests
{
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]
public void Only_Calls_LayoutManager_InvalidateMeasure_Once()
{
@@ -86,5 +177,24 @@ namespace Avalonia.Layout.UnitTests
AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager);
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);
+ }
+ }
}
}
diff --git a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
index 4ef864d843..63da9ed3f0 100644
--- a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
+++ b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
@@ -5,7 +5,7 @@ using Avalonia.Platform;
namespace Avalonia.UnitTests
{
- public class MockStreamGeometryImpl : IStreamGeometryImpl
+ public class MockStreamGeometryImpl : IStreamGeometryImpl, ITransformedGeometryImpl
{
private MockStreamGeometryContext _context;
@@ -27,6 +27,8 @@ namespace Avalonia.UnitTests
_context = context;
}
+ public IGeometryImpl SourceGeometry { get; }
+
public Rect Bounds => _context.CalculateBounds();
public Matrix Transform { get; }
@@ -36,6 +38,10 @@ namespace Avalonia.UnitTests
return this;
}
+ public void Dispose()
+ {
+ }
+
public bool FillContains(Point point)
{
return _context.FillContains(point);
@@ -46,7 +52,7 @@ namespace Avalonia.UnitTests
return false;
}
- public Rect GetRenderBounds(double strokeThickness) => Bounds;
+ public Rect GetRenderBounds(Pen pen) => Bounds;
public IGeometryImpl Intersect(IGeometryImpl geometry)
{
@@ -58,7 +64,7 @@ namespace Avalonia.UnitTests
return _context;
}
- public IGeometryImpl WithTransform(Matrix transform)
+ public ITransformedGeometryImpl WithTransform(Matrix transform)
{
return new MockStreamGeometryImpl(transform, _context);
}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs
new file mode 100644
index 0000000000..b046910f34
--- /dev/null
+++ b/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 FooProperty =
+ AvaloniaProperty.Register(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(
+ x => x.WithTransform(It.IsAny()) ==
+ Mock.Of(y =>
+ y.SourceGeometry == x));
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/RectangleGeometryTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/RectangleGeometryTests.cs
new file mode 100644
index 0000000000..5af1bb572e
--- /dev/null
+++ b/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();
+ var transformedGeometry = new Mock();
+ var streamGeometry = Mock.Of(x =>
+ x.Open() == context &&
+ x.WithTransform(It.IsAny()) == transformedGeometry.Object);
+ var renderInterface = Mock.Of(x =>
+ x.CreateStreamGeometry() == streamGeometry);
+ return new TestServices(renderInterface: renderInterface);
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
index 5fcf1cf1f2..54bb5d72d0 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
@@ -65,22 +65,13 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
}
}
- public Matrix Transform
+ public IStreamGeometryImpl Clone()
{
- get
- {
- throw new NotImplementedException();
- }
-
- set
- {
- throw new NotImplementedException();
- }
+ return this;
}
- public IStreamGeometryImpl Clone()
+ public void Dispose()
{
- return this;
}
public bool FillContains(Point point)
@@ -88,7 +79,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
return _impl.FillContains(point);
}
- public Rect GetRenderBounds(double strokeThickness)
+ public Rect GetRenderBounds(Pen pen)
{
throw new NotImplementedException();
}
@@ -108,7 +99,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
- public IGeometryImpl WithTransform(Matrix transform)
+ public ITransformedGeometryImpl WithTransform(Matrix transform)
{
throw new NotImplementedException();
}