Browse Source

Merge branch 'master' into fixes/497-shared-contextmenu

pull/3751/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
d396d7ed3a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      packages/Avalonia/AvaloniaBuildTasks.targets
  2. 53
      samples/RenderDemo/Controls/LineBoundsDemoControl.cs
  3. 3
      samples/RenderDemo/MainWindow.xaml
  4. 9
      samples/RenderDemo/Pages/LineBoundsPage.xaml
  5. 19
      samples/RenderDemo/Pages/LineBoundsPage.xaml.cs
  6. 3
      samples/RenderDemo/RenderDemo.csproj
  7. 11
      src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs
  8. 9
      src/Avalonia.Build.Tasks/Extensions.cs
  9. 31
      src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
  10. 4
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  11. 2
      src/Avalonia.Controls/AutoCompleteBox.cs
  12. 35
      src/Avalonia.Controls/SelectionModel.cs
  13. 15
      src/Avalonia.Visuals/Media/PixelRect.cs
  14. 11
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  15. 4
      src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs
  16. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs
  17. 4
      src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs
  18. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  19. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs
  20. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
  21. 68
      src/Avalonia.Visuals/Rendering/SceneGraph/LineBoundsHelper.cs
  22. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
  23. 4
      src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs
  24. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  25. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs
  26. 2
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  27. 40
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  28. 54
      tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs
  29. 46
      tests/Avalonia.Visuals.UnitTests/Media/PixelRectTests.cs
  30. 17
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

5
packages/Avalonia/AvaloniaBuildTasks.targets

@ -2,6 +2,7 @@
<PropertyGroup>
<_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild)</_AvaloniaUseExternalMSBuild>
<_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false</_AvaloniaUseExternalMSBuild>
<AvaloniaXamlReportImportance Condition="'$(AvaloniaXamlReportImportance)' == ''">low</AvaloniaXamlReportImportance>
</PropertyGroup>
<UsingTask TaskName="GenerateAvaloniaResourcesTask"
@ -38,7 +39,8 @@
Output="$(AvaloniaResourcesTemporaryFilePath)"
Root="$(MSBuildProjectDirectory)"
Resources="@(AvaloniaResource)"
EmbeddedResources="@(EmbeddedResources)"/>
EmbeddedResources="@(EmbeddedResources)"
ReportImportance="$(AvaloniaXamlReportImportance)"/>
<Exec
Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"
Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
@ -67,6 +69,7 @@
OriginalCopyPath="$(AvaloniaXamlOriginalCopyFilePath)"
ProjectDirectory="$(MSBuildProjectDirectory)"
VerifyIl="$(AvaloniaXamlIlVerifyIl)"
ReportImportance="$(AvaloniaXamlReportImportance)"
/>
<Exec
Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"

53
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<LineBoundsDemoControl>(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<double> AngleProperty =
AvaloniaProperty.Register<LineBoundsDemoControl, double>(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));
}
}
}

3
samples/RenderDemo/MainWindow.xaml

@ -44,6 +44,9 @@
<TabItem Header="GlyphRun">
<pages:GlyphRunPage/>
</TabItem>
<TabItem Header="LineBounds">
<pages:LineBoundsPage />
</TabItem>
</TabControl>
</DockPanel>
</Window>

9
samples/RenderDemo/Pages/LineBoundsPage.xaml

@ -0,0 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:controls="clr-namespace:RenderDemo.Controls"
x:Class="RenderDemo.Pages.LineBoundsPage">
<controls:LineBoundsDemoControl />
</UserControl>

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

3
samples/RenderDemo/RenderDemo.csproj

@ -3,6 +3,9 @@
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\src\Avalonia.Visuals\Rendering\SceneGraph\LineBoundsHelper.cs" Link="LineBoundsHelper.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />

11
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; }
}

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

31
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<Source> BuildResourceSources() => Resources.Select(r => new Source(r.ItemSpec, Root)).ToList();
List<Source> 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<Source> 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<MessageImportance>(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();

4
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);

2
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)

35
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<IndexPath>? _selectedIndicesCached;
private IReadOnlyList<object?>? _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;
}
}

15
src/Avalonia.Visuals/Media/PixelRect.cs

@ -377,7 +377,7 @@ namespace Avalonia
/// <returns>The device-independent rect.</returns>
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)));
/// <summary>
/// Converts a <see cref="Rect"/> to device pixels using the specified scaling factor.
@ -387,7 +387,7 @@ namespace Avalonia
/// <returns>The device-independent point.</returns>
public static PixelRect FromRect(Rect rect, Vector scale) => new PixelRect(
PixelPoint.FromPoint(rect.Position, scale),
PixelSize.FromSize(rect.Size, scale));
FromPointCeiling(rect.BottomRight, scale));
/// <summary>
/// Converts a <see cref="Rect"/> to device pixels using the specified dots per inch (DPI).
@ -397,7 +397,7 @@ namespace Avalonia
/// <returns>The device-independent point.</returns>
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)));
/// <summary>
/// Converts a <see cref="Rect"/> to device pixels using the specified dots per inch (DPI).
@ -407,7 +407,7 @@ namespace Avalonia
/// <returns>The device-independent point.</returns>
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));
/// <summary>
/// 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));
}
}
}

11
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)

4
src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs

@ -9,8 +9,8 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
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)
{
}

2
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;

4
src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs

@ -9,9 +9,9 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
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)));

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

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

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

@ -25,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph
GlyphRun glyphRun,
Point baselineOrigin,
IDictionary<IVisual, Scene> childScenes = null)
: base(glyphRun.Bounds, transform, null)
: base(glyphRun.Bounds, transform)
{
Transform = transform;
Foreground = foreground?.ToImmutable();

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

@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="destRect">The destination rect.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
: base(destRect, transform, null)
: base(destRect, transform)
{
Transform = transform;
Source = source.Clone();

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

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

@ -25,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph
Point p1,
Point p2,
IDictionary<IVisual, Scene> childScenes = null)
: base(new Rect(p1, p2), transform, pen)
: base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform)
{
Transform = transform;
Pen = pen?.ToImmutable();

4
src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs

@ -18,7 +18,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="bounds">The bounds of the mask.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary<IVisual, Scene> 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.
/// </summary>
public OpacityMaskNode()
: base(Rect.Empty, Matrix.Identity, null)
: base(Rect.Empty, Matrix.Identity)
{
}

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

@ -28,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph
RoundedRect rect,
BoxShadows boxShadows,
IDictionary<IVisual, Scene> 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();

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

@ -24,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph
Point origin,
IFormattedTextImpl text,
IDictionary<IVisual, Scene> childScenes = null)
: base(text.Bounds.Translate(origin), transform, null)
: base(text.Bounds.Translate(origin), transform)
{
Transform = transform;
Foreground = foreground?.ToImmutable();

2
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;
}
/// <inheritdoc />

40
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<string>((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<ListBox>((parent, scope) =>

54
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()
{

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

17
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)
{
}

Loading…
Cancel
Save