diff --git a/samples/RenderDemo/Controls/LineBoundsDemoControl.cs b/samples/RenderDemo/Controls/LineBoundsDemoControl.cs new file mode 100644 index 0000000000..0e0b3d6142 --- /dev/null +++ b/samples/RenderDemo/Controls/LineBoundsDemoControl.cs @@ -0,0 +1,53 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; + +namespace RenderDemo.Controls +{ + public class LineBoundsDemoControl : Control + { + static LineBoundsDemoControl() + { + AffectsRender(AngleProperty); + } + + public LineBoundsDemoControl() + { + var timer = new DispatcherTimer(); + timer.Interval = TimeSpan.FromSeconds(1 / 60); + timer.Tick += (sender, e) => Angle += Math.PI / 360; + timer.Start(); + } + + public static readonly StyledProperty AngleProperty = + AvaloniaProperty.Register(nameof(Angle)); + + public double Angle + { + get => GetValue(AngleProperty); + set => SetValue(AngleProperty, value); + } + + public override void Render(DrawingContext drawingContext) + { + var lineLength = Math.Sqrt((100 * 100) + (100 * 100)); + + var diffX = LineBoundsHelper.CalculateAdjSide(Angle, lineLength); + var diffY = LineBoundsHelper.CalculateOppSide(Angle, lineLength); + + + var p1 = new Point(200, 200); + var p2 = new Point(p1.X + diffX, p1.Y + diffY); + + var pen = new Pen(Brushes.Green, 20, lineCap: PenLineCap.Square); + var boundPen = new Pen(Brushes.Black); + + drawingContext.DrawLine(pen, p1, p2); + + drawingContext.DrawRectangle(boundPen, LineBoundsHelper.CalculateBounds(p1, p2, pen)); + } + } +} diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index b17520a466..c098ef411e 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -44,6 +44,9 @@ + + + diff --git a/samples/RenderDemo/Pages/LineBoundsPage.xaml b/samples/RenderDemo/Pages/LineBoundsPage.xaml new file mode 100644 index 0000000000..07d658630a --- /dev/null +++ b/samples/RenderDemo/Pages/LineBoundsPage.xaml @@ -0,0 +1,9 @@ + + + diff --git a/samples/RenderDemo/Pages/LineBoundsPage.xaml.cs b/samples/RenderDemo/Pages/LineBoundsPage.xaml.cs new file mode 100644 index 0000000000..28ddedd4bc --- /dev/null +++ b/samples/RenderDemo/Pages/LineBoundsPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace RenderDemo.Pages +{ + public class LineBoundsPage : UserControl + { + public LineBoundsPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj index ce33f42143..0d7d62e177 100644 --- a/samples/RenderDemo/RenderDemo.csproj +++ b/samples/RenderDemo/RenderDemo.csproj @@ -3,6 +3,9 @@ Exe netcoreapp3.1 + + + diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 3e4f47ec8a..b38cc56a17 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -10,6 +10,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; @@ -1100,6 +1101,7 @@ namespace Avalonia.Controls { _textBoxSubscriptions = _textBox.GetObservable(TextBox.TextProperty) + .Skip(1) .Subscribe(_ => OnTextBoxTextChanged()); if (Text != null) diff --git a/src/Avalonia.Visuals/Media/PixelRect.cs b/src/Avalonia.Visuals/Media/PixelRect.cs index c062112ace..32a728475f 100644 --- a/src/Avalonia.Visuals/Media/PixelRect.cs +++ b/src/Avalonia.Visuals/Media/PixelRect.cs @@ -377,7 +377,7 @@ namespace Avalonia /// The device-independent rect. public static PixelRect FromRect(Rect rect, double scale) => new PixelRect( PixelPoint.FromPoint(rect.Position, scale), - PixelSize.FromSize(rect.Size, scale)); + FromPointCeiling(rect.BottomRight, new Vector(scale, scale))); /// /// Converts a to device pixels using the specified scaling factor. @@ -387,7 +387,7 @@ namespace Avalonia /// The device-independent point. public static PixelRect FromRect(Rect rect, Vector scale) => new PixelRect( PixelPoint.FromPoint(rect.Position, scale), - PixelSize.FromSize(rect.Size, scale)); + FromPointCeiling(rect.BottomRight, scale)); /// /// Converts a to device pixels using the specified dots per inch (DPI). @@ -397,7 +397,7 @@ namespace Avalonia /// The device-independent point. public static PixelRect FromRectWithDpi(Rect rect, double dpi) => new PixelRect( PixelPoint.FromPointWithDpi(rect.Position, dpi), - PixelSize.FromSizeWithDpi(rect.Size, dpi)); + FromPointCeiling(rect.BottomRight, new Vector(dpi / 96, dpi / 96))); /// /// Converts a to device pixels using the specified dots per inch (DPI). @@ -407,7 +407,7 @@ namespace Avalonia /// The device-independent point. public static PixelRect FromRectWithDpi(Rect rect, Vector dpi) => new PixelRect( PixelPoint.FromPointWithDpi(rect.Position, dpi), - PixelSize.FromSizeWithDpi(rect.Size, dpi)); + FromPointCeiling(rect.BottomRight, dpi / 96)); /// /// Returns the string representation of the rectangle. @@ -441,5 +441,12 @@ namespace Avalonia ); } } + + private static PixelPoint FromPointCeiling(Point point, Vector scale) + { + return new PixelPoint( + (int)Math.Ceiling(point.X * scale.X), + (int)Math.Ceiling(point.Y * scale.Y)); + } } } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 0e6dda1710..59dd369956 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -443,11 +443,12 @@ namespace Avalonia.Rendering private static Rect SnapToDevicePixels(Rect rect, double scale) { return new Rect( - Math.Floor(rect.X * scale) / scale, - Math.Floor(rect.Y * scale) / scale, - Math.Ceiling(rect.Width * scale) / scale, - Math.Ceiling(rect.Height * scale) / scale); - + new Point( + Math.Floor(rect.X * scale) / scale, + Math.Floor(rect.Y * scale) / scale), + new Point( + Math.Ceiling(rect.Right * scale) / scale, + Math.Ceiling(rect.Bottom * scale) / scale)); } private void RenderOverlay(Scene scene, ref IDrawingContextImpl parentContent) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs index 315076570e..c559f05d70 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs @@ -9,8 +9,8 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class BrushDrawOperation : DrawOperation { - public BrushDrawOperation(Rect bounds, Matrix transform, IPen pen) - : base(bounds, transform, pen) + public BrushDrawOperation(Rect bounds, Matrix transform) + : base(bounds, transform) { } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs index 68e2237430..15e5660671 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs @@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph public Matrix Transform { get; } public ICustomDrawOperation Custom { get; } public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform) - : base(custom.Bounds, transform, null) + : base(custom.Bounds, transform) { Transform = transform; Custom = custom; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs index d9dfd8bd55..0b04b97ff2 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs @@ -9,9 +9,9 @@ namespace Avalonia.Rendering.SceneGraph /// internal abstract class DrawOperation : IDrawOperation { - public DrawOperation(Rect bounds, Matrix transform, IPen pen) + public DrawOperation(Rect bounds, Matrix transform) { - bounds = bounds.Inflate((pen?.Thickness ?? 0) / 2).TransformToAABB(transform); + bounds = bounds.TransformToAABB(transform); Bounds = new Rect( new Point(Math.Floor(bounds.X), Math.Floor(bounds.Y)), new Point(Math.Ceiling(bounds.Right), Math.Ceiling(bounds.Bottom))); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index cb7498f7b7..8a19679c77 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -24,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph IPen pen, IGeometryImpl geometry, IDictionary childScenes = null) - : base(geometry.GetRenderBounds(pen), transform, null) + : base(geometry.GetRenderBounds(pen), transform) { Transform = transform; Brush = brush?.ToImmutable(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs index 9f314a5727..eaf4effdbe 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs @@ -25,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph GlyphRun glyphRun, Point baselineOrigin, IDictionary childScenes = null) - : base(glyphRun.Bounds, transform, null) + : base(glyphRun.Bounds, transform) { Transform = transform; Foreground = foreground?.ToImmutable(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index d1bc261c7a..c9052c6ef2 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The destination rect. /// The bitmap interpolation mode. public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) - : base(destRect, transform, null) + : base(destRect, transform) { Transform = transform; Source = source.Clone(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineBoundsHelper.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineBoundsHelper.cs new file mode 100644 index 0000000000..56d218e398 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineBoundsHelper.cs @@ -0,0 +1,68 @@ +using System; +using Avalonia.Media; + +namespace Avalonia.Rendering.SceneGraph +{ + internal static class LineBoundsHelper + { + private static double CalculateAngle(Point p1, Point p2) + { + var xDiff = p2.X - p1.X; + var yDiff = p2.Y - p1.Y; + + return Math.Atan2(yDiff, xDiff); + } + + internal static double CalculateOppSide(double angle, double hyp) + { + return Math.Sin(angle) * hyp; + } + + internal static double CalculateAdjSide(double angle, double hyp) + { + return Math.Cos(angle) * hyp; + } + + private static (Point p1, Point p2) TranslatePointsAlongTangent(Point p1, Point p2, double angle, double distance) + { + var xDiff = CalculateOppSide(angle, distance); + var yDiff = CalculateAdjSide(angle, distance); + + var c1 = new Point(p1.X + xDiff, p1.Y - yDiff); + var c2 = new Point(p1.X - xDiff, p1.Y + yDiff); + + var c3 = new Point(p2.X + xDiff, p2.Y - yDiff); + var c4 = new Point(p2.X - xDiff, p2.Y + yDiff); + + var minX = Math.Min(c1.X, Math.Min(c2.X, Math.Min(c3.X, c4.X))); + var minY = Math.Min(c1.Y, Math.Min(c2.Y, Math.Min(c3.Y, c4.Y))); + var maxX = Math.Max(c1.X, Math.Max(c2.X, Math.Max(c3.X, c4.X))); + var maxY = Math.Max(c1.Y, Math.Max(c2.Y, Math.Max(c3.Y, c4.Y))); + + return (new Point(minX, minY), new Point(maxX, maxY)); + } + + private static Rect CalculateBounds(Point p1, Point p2, double thickness, double angleToCorner) + { + var pts = TranslatePointsAlongTangent(p1, p2, angleToCorner, thickness / 2); + + return new Rect(pts.p1, pts.p2); + } + + public static Rect CalculateBounds(Point p1, Point p2, IPen p) + { + var radians = CalculateAngle(p1, p2); + + if (p.LineCap != PenLineCap.Flat) + { + var pts = TranslatePointsAlongTangent(p1, p2, radians - Math.PI / 2, p.Thickness / 2); + + return CalculateBounds(pts.p1, pts.p2, p.Thickness, radians); + } + else + { + return CalculateBounds(p1, p2, p.Thickness, radians); + } + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index f00f23e08b..54a9ff733d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -25,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph Point p1, Point p2, IDictionary childScenes = null) - : base(new Rect(p1, p2), transform, pen) + : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) { Transform = transform; Pen = pen?.ToImmutable(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs index d6dbc1a8cb..b8e7b150ac 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs @@ -18,7 +18,7 @@ namespace Avalonia.Rendering.SceneGraph /// The bounds of the mask. /// Child scenes for drawing visual brushes. public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary childScenes = null) - : base(Rect.Empty, Matrix.Identity, null) + : base(Rect.Empty, Matrix.Identity) { Mask = mask?.ToImmutable(); MaskBounds = bounds; @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() - : base(Rect.Empty, Matrix.Identity, null) + : base(Rect.Empty, Matrix.Identity) { } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 633b1fc5f3..5059a6d042 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -28,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph RoundedRect rect, BoxShadows boxShadows, IDictionary childScenes = null) - : base(boxShadows.TransformBounds(rect.Rect), transform, pen) + : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform) { Transform = transform; Brush = brush?.ToImmutable(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs index 336207dedf..4b6c331023 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs @@ -24,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph Point origin, IFormattedTextImpl text, IDictionary childScenes = null) - : base(text.Bounds.Translate(origin), transform, null) + : base(text.Bounds.Translate(origin), transform) { Transform = transform; Foreground = foreground?.ToImmutable(); diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 1d78609f5d..879b18742e 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -95,7 +95,7 @@ namespace Avalonia.Skia UpdatePathCache(strokeWidth); } - return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0); + return _pathCache.CachedGeometryRenderBounds; } /// diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PixelRectTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PixelRectTests.cs new file mode 100644 index 0000000000..c6da9b77e9 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/PixelRectTests.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class PixelRectTests + { + [Fact] + public void FromRect_Snaps_To_Device_Pixels() + { + var rect = new Rect(189, 189, 26, 164); + var result = PixelRect.FromRect(rect, 1.5); + + Assert.Equal(new PixelRect(283, 283, 40, 247), result); + } + + [Fact] + public void FromRect_Vector_Snaps_To_Device_Pixels() + { + var rect = new Rect(189, 189, 26, 164); + var result = PixelRect.FromRect(rect, new Vector(1.5, 1.5)); + + Assert.Equal(new PixelRect(283, 283, 40, 247), result); + } + + [Fact] + public void FromRectWithDpi_Snaps_To_Device_Pixels() + { + var rect = new Rect(189, 189, 26, 164); + var result = PixelRect.FromRectWithDpi(rect, 144); + + Assert.Equal(new PixelRect(283, 283, 40, 247), result); + } + + [Fact] + public void FromRectWithDpi_Vector_Snaps_To_Device_Pixels() + { + var rect = new Rect(189, 189, 26, 164); + var result = PixelRect.FromRectWithDpi(rect, new Vector(144, 144)); + + Assert.Equal(new PixelRect(283, 283, 40, 247), result); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 7787ac0871..0cdc8827ad 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -35,7 +35,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph double expectedWidth, double expectedHeight) { - var target = new TestDrawOperation( + var target = new TestRectangleDrawOperation( new Rect(x, y, width, height), Matrix.CreateScale(scaleX, scaleY), penThickness.HasValue ? new Pen(Brushes.Black, penThickness.Value) : null); @@ -74,10 +74,23 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph geometryNode.HitTest(new Point()); } + private class TestRectangleDrawOperation : RectangleNode + { + public TestRectangleDrawOperation(Rect bounds, Matrix transform, Pen pen) + : base(transform, pen.Brush, pen, bounds, new BoxShadows()) + { + + } + + public override bool HitTest(Point p) => false; + + public override void Render(IDrawingContextImpl context) { } + } + private class TestDrawOperation : DrawOperation { public TestDrawOperation(Rect bounds, Matrix transform, Pen pen) - :base(bounds, transform, pen) + :base(bounds, transform) { }