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