diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 552713f94b..537495fcad 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -2,6 +2,7 @@ <_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild) <_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false + low + EmbeddedResources="@(EmbeddedResources)" + ReportImportance="$(AvaloniaXamlReportImportance)"/> @@ -67,6 +69,7 @@ OriginalCopyPath="$(AvaloniaXamlOriginalCopyFilePath)" ProjectDirectory="$(MSBuildProjectDirectory)" VerifyIl="$(AvaloniaXamlIlVerifyIl)" + ReportImportance="$(AvaloniaXamlReportImportance)" /> (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.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs index 39ee3f6bca..95e59dde2b 100644 --- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -12,6 +12,8 @@ namespace Avalonia.Build.Tasks { public bool Execute() { + Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance); + OutputPath = OutputPath ?? AssemblyFile; var outputPdb = GetPdbPath(OutputPath); var input = AssemblyFile; @@ -32,9 +34,12 @@ namespace Avalonia.Build.Tasks } } + var msg = $"CompileAvaloniaXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}"; + BuildEngine.LogMessage(msg, outputImportance < MessageImportance.Low ? MessageImportance.High : outputImportance); + var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input, File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), - ProjectDirectory, OutputPath, VerifyIl); + ProjectDirectory, OutputPath, VerifyIl, outputImportance); if (!res.Success) return false; if (!res.WrittenFile) @@ -68,7 +73,9 @@ namespace Avalonia.Build.Tasks public string OutputPath { get; set; } public bool VerifyIl { get; set; } - + + public string ReportImportance { get; set; } + public IBuildEngine BuildEngine { get; set; } public ITaskHost HostObject { get; set; } } diff --git a/src/Avalonia.Build.Tasks/Extensions.cs b/src/Avalonia.Build.Tasks/Extensions.cs index 440c6d7489..46c12eaf3d 100644 --- a/src/Avalonia.Build.Tasks/Extensions.cs +++ b/src/Avalonia.Build.Tasks/Extensions.cs @@ -9,14 +9,19 @@ namespace Avalonia.Build.Tasks public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message) { - engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, + engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, "", "Avalonia")); } - + public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message) { engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message, "", "Avalonia")); } + + public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp) + { + engine.LogMessageEvent(new BuildMessageEventArgs(message, "", "Avalonia", imp)); + } } } diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index 98ebb3e7d1..406abe6f99 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -22,6 +22,10 @@ namespace Avalonia.Build.Tasks [Required] public ITaskItem[] EmbeddedResources { get; set; } + public string ReportImportance { get; set; } + + private MessageImportance _reportImportance; + class Source { public string Path { get; set; } @@ -29,15 +33,11 @@ namespace Avalonia.Build.Tasks private byte[] _data; private string _sourcePath; - public Source(string file, string root) + public Source(string relativePath, string root) { - file = SPath.GetFullPath(file); root = SPath.GetFullPath(root); - var fileUri = new Uri(file, UriKind.Absolute); - var rootUri = new Uri(root, UriKind.Absolute); - rootUri = new Uri(rootUri.ToString().TrimEnd('/') + '/'); - Path = '/' + rootUri.MakeRelativeUri(fileUri).ToString().TrimStart('/'); - _sourcePath = file; + Path = "/" + relativePath.Replace('\\', '/'); + _sourcePath = SPath.Combine(root, relativePath); Size = (int)new FileInfo(_sourcePath).Length; } @@ -65,7 +65,14 @@ namespace Avalonia.Build.Tasks } } - List BuildResourceSources() => Resources.Select(r => new Source(r.ItemSpec, Root)).ToList(); + List BuildResourceSources() + => Resources.Select(r => + { + + var src = new Source(r.ItemSpec, Root); + BuildEngine.LogMessage($"avares -> name:{src.Path}, path: {src.SystemPath}, size:{src.Size}, ItemSpec:{r.ItemSpec}", _reportImportance); + return src; + }).ToList(); private void Pack(Stream output, List sources) { @@ -136,10 +143,14 @@ namespace Avalonia.Build.Tasks sources.Add(new Source("/!AvaloniaResourceXamlInfo", ms.ToArray())); return true; } - + public bool Execute() { - foreach(var r in EmbeddedResources.Where(r=>r.ItemSpec.EndsWith(".xaml")||r.ItemSpec.EndsWith(".paml"))) + Enum.TryParse(ReportImportance, out _reportImportance); + + BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance); + + foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml"))) BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec, "XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work"); var resources = BuildResourceSources(); diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index e348eb0fbc..3b69109e68 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -40,7 +40,7 @@ namespace Avalonia.Build.Tasks } public static CompileResult Compile(IBuildEngine engine, string input, string[] references, string projectDirectory, - string output, bool verifyIl) + string output, bool verifyIl, MessageImportance logImportance) { var typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input); var asm = typeSystem.TargetAssemblyDefinition; @@ -121,6 +121,8 @@ namespace Avalonia.Build.Tasks { try { + engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance); + // StreamReader is needed here to handle BOM var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd(); var parsed = XDocumentXamlIlParser.Parse(xaml); 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.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index dd4934f9e5..93699583e6 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -20,6 +20,7 @@ namespace Avalonia.Controls private bool _singleSelect; private bool _autoSelect; private int _operationCount; + private IndexPath _oldAnchorIndex; private IReadOnlyList? _selectedIndicesCached; private IReadOnlyList? _selectedItemsCached; private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs; @@ -142,6 +143,8 @@ namespace Avalonia.Controls } set { + var oldValue = AnchorIndex; + if (value != null) { SelectionTreeHelper.TraverseIndexPath( @@ -155,7 +158,10 @@ namespace Avalonia.Controls _rootNode.AnchorIndex = -1; } - RaisePropertyChanged("AnchorIndex"); + if (_operationCount == 0 && oldValue != AnchorIndex) + { + RaisePropertyChanged("AnchorIndex"); + } } } @@ -633,19 +639,18 @@ namespace Avalonia.Controls _selectedIndicesCached = null; _selectedItemsCached = null; - // Raise SelectionChanged event if (e != null) { SelectionChanged?.Invoke(this, e); - } - RaisePropertyChanged(nameof(SelectedIndex)); - RaisePropertyChanged(nameof(SelectedIndices)); + RaisePropertyChanged(nameof(SelectedIndex)); + RaisePropertyChanged(nameof(SelectedIndices)); - if (_rootNode.Source != null) - { - RaisePropertyChanged(nameof(SelectedItem)); - RaisePropertyChanged(nameof(SelectedItems)); + if (_rootNode.Source != null) + { + RaisePropertyChanged(nameof(SelectedItem)); + RaisePropertyChanged(nameof(SelectedItems)); + } } } @@ -785,6 +790,7 @@ namespace Avalonia.Controls { if (_operationCount++ == 0) { + _oldAnchorIndex = AnchorIndex; _rootNode.BeginOperation(); } } @@ -808,13 +814,16 @@ namespace Avalonia.Controls var changeSet = new SelectionModelChangeSet(changes); e = changeSet.CreateEventArgs(); } - } - OnSelectionChanged(e); + OnSelectionChanged(e); + + if (_oldAnchorIndex != AnchorIndex) + { + RaisePropertyChanged(nameof(AnchorIndex)); + } - if (_operationCount == 0) - { _rootNode.Cleanup(); + _oldAnchorIndex = default; } } 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.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index d43385142d..a7679ba388 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -367,6 +367,46 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Clicking_Item_Should_Raise_BringIntoView_For_Correct_Control() + { + // Issue #3934 + var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray(); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), + SelectionMode = SelectionMode.AlwaysSelected, + VirtualizationMode = ItemVirtualizationMode.None, + }; + + Prepare(target); + + // First an item that is not index 0 must be selected. + _mouse.Click(target.Presenter.Panel.Children[1]); + Assert.Equal(new IndexPath(1), target.Selection.AnchorIndex); + + // We're going to be clicking on item 9. + var item = (ListBoxItem)target.Presenter.Panel.Children[9]; + var raised = 0; + + // Make sure a RequestBringIntoView event is raised for item 9. It won't be handled + // by the ScrollContentPresenter as the item is already visible, so we don't need + // handledEventsToo: true. Issue #3934 failed here because item 0 was being scrolled + // into view due to SelectionMode.AlwaysSelected. + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => + { + Assert.Same(item, e.TargetObject); + ++raised; + }); + + // Click item 9. + _mouse.Click(item); + + Assert.Equal(1, raised); + } + private FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate((parent, scope) => diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index 246ff723a1..ebf9c40012 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1458,6 +1458,60 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, raised); } + [Fact] + public void Batch_Update_Does_Not_Raise_PropertyChanged_Until_Operation_Finished() + { + var data = new[] { "foo", "bar", "baz", "qux" }; + var target = new SelectionModel { Source = data }; + var raised = 0; + + target.SelectedIndex = new IndexPath(1); + + Assert.Equal(new IndexPath(1), target.AnchorIndex); + + target.PropertyChanged += (s, e) => ++raised; + + using (target.Update()) + { + target.ClearSelection(); + + Assert.Equal(0, raised); + + target.AnchorIndex = new IndexPath(2); + + Assert.Equal(0, raised); + + target.SelectedIndex = new IndexPath(3); + + Assert.Equal(0, raised); + } + + Assert.Equal(new IndexPath(3), target.AnchorIndex); + Assert.Equal(5, raised); + } + + [Fact] + public void Batch_Update_Does_Not_Raise_PropertyChanged_If_Nothing_Changed() + { + var data = new[] { "foo", "bar", "baz", "qux" }; + var target = new SelectionModel { Source = data }; + var raised = 0; + + target.SelectedIndex = new IndexPath(1); + + Assert.Equal(new IndexPath(1), target.AnchorIndex); + + target.PropertyChanged += (s, e) => ++raised; + + using (target.Update()) + { + target.ClearSelection(); + target.SelectedIndex = new IndexPath(1); + } + + Assert.Equal(0, raised); + } + [Fact] public void AutoSelect_Selects_When_Enabled() { 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) { }