From 92a2f25de44afb1db346a9c9d8d806b15ab18321 Mon Sep 17 00:00:00 2001 From: donandren Date: Fri, 20 May 2016 17:32:09 +0300 Subject: [PATCH 001/122] make sure member selector is evaluating target value every time select is made --- .../Templates/MemberSelector.cs | 2 ++ .../Templates/MemberSelectorTests.cs | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs index a901c175c9..be4287605c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs @@ -51,6 +51,8 @@ namespace Avalonia.Markup.Xaml.Templates object result = _memberValueNode.CurrentValue.Target; + _expressionNode.Target = null; + if (result == AvaloniaProperty.UnsetValue) { return null; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs index 4903ca7b35..7082863c0d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs @@ -157,6 +157,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Templates Assert.Equal(1, selector.Select(data)); } + [Fact] + public void Should_Support_Change_Of_Target_Value() + { + var selector = new MemberSelector() { MemberName = "StringValue" }; + + var data = new Item() + { + StringValue = "Value1" + }; + + Assert.Same("Value1", selector.Select(data)); + + data.StringValue = "Value2"; + + Assert.Same("Value2", selector.Select(data)); + } + private class Item { public Item Child { get; set; } From 4fef64037179a49d3894fcda5d8a1fb68e1e7725 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 22 May 2016 20:24:51 -0400 Subject: [PATCH 002/122] Updated adorners and hit testing to account for RenderTransforms (#538) Updated adorners and hit testing to account for RenderTransforms. Fixes #433. --- Avalonia.sln | 12 +- .../Primitives/AdornerLayer.cs | 3 + src/Avalonia.Input/InputExtensions.cs | 13 +- .../Media/DrawingContext.cs | 4 +- src/Avalonia.SceneGraph/Media/Geometry.cs | 10 + .../Platform/IGeometryImpl.cs | 7 + .../Rendering/RendererMixin.cs | 6 + .../VisualTree/BoundsTracker.cs | 41 +-- .../VisualTree/TransformedBounds.cs | 24 +- .../Media/StreamGeometryContextImpl.cs | 5 + .../Media/StreamGeometryImpl.cs | 5 + src/Skia/Avalonia.Skia/StreamGeometryImpl.cs | 7 + src/Skia/Avalonia.Skia/readme.md | 3 + .../Avalonia.Direct2D1/Media/GeometryImpl.cs | 7 + .../Avalonia.Input.UnitTests.csproj | 6 +- .../InputElement_HitTesting.cs | 316 +++++++++++++++--- .../Avalonia.Input.UnitTests/packages.config | 1 + .../Avalonia.SceneGraph.UnitTests.csproj | 6 +- .../VisualTree/BoundsTrackerTests.cs | 51 +-- 19 files changed, 414 insertions(+), 113 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index d9ef7a7137..9af5841a0b 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject @@ -159,19 +159,25 @@ EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4 + src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4 + src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{db070a10-bf39-4752-8456-86e9d5928478}*SharedItemsImports = 4 - src\Shared\RenderHelpers\RenderHelpers.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4 + src\Shared\RenderHelpers\RenderHelpers.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{78345174-5b52-4a14-b9fd-d5f2428137f0}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{54f237d5-a70a-4752-9656-0c70b1a7b047}*SharedItemsImports = 4 + samples\TestApplicationShared\TestApplicationShared.projitems*{ff69b927-c545-49ae-8e16-3d14d621aa12}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{811a76cf-1cf6-440f-963b-bbe31bd72a82}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 4 - src\Shared\RenderHelpers\RenderHelpers.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 + src\Shared\RenderHelpers\RenderHelpers.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 + samples\TestApplicationShared\TestApplicationShared.projitems*{8c923867-8a8f-4f6b-8b80-47d9e8436166}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{e3a1060b-50d0-44e8-88b6-f44ef2e5bd72}*SharedItemsImports = 4 + src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 + src\Shared\RenderHelpers\RenderHelpers.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{e1aa3dbf-9056-4530-9376-18119a7a3ffe}*SharedItemsImports = 4 EndGlobalSection diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index abdd890ecd..b732387cf5 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; using System.Linq; using Avalonia.VisualTree; +using Avalonia.Media; namespace Avalonia.Controls.Primitives { @@ -58,6 +59,8 @@ namespace Avalonia.Controls.Primitives if (info != null) { + child.RenderTransform = new MatrixTransform(info.Bounds.Transform); + child.TransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute); child.Arrange(info.Bounds.Bounds); } else diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index fcf82287ec..a2321f3493 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -1,6 +1,7 @@ // 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.VisualTree; using System; using System.Collections.Generic; using System.Linq; @@ -23,14 +24,13 @@ namespace Avalonia.Input public static IEnumerable GetInputElementsAt(this IInputElement element, Point p) { Contract.Requires(element != null); + var transformedBounds = BoundsTracker.GetTransformedBounds((Visual)element); + var geometry = transformedBounds.GetTransformedBoundsGeometry(); - if (element.Bounds.Contains(p) && - element.IsVisible && + if (element.IsVisible && element.IsHitTestVisible && element.IsEnabledCore) { - p -= element.Bounds.Position; - if (element.VisualChildren.Any()) { foreach (var child in ZSort(element.VisualChildren.OfType())) @@ -42,7 +42,10 @@ namespace Avalonia.Input } } - yield return element; + if (geometry.FillContains(p)) + { + yield return element; + } } } diff --git a/src/Avalonia.SceneGraph/Media/DrawingContext.cs b/src/Avalonia.SceneGraph/Media/DrawingContext.cs index c12434a442..90a9749e2a 100644 --- a/src/Avalonia.SceneGraph/Media/DrawingContext.cs +++ b/src/Avalonia.SceneGraph/Media/DrawingContext.cs @@ -44,6 +44,8 @@ namespace Avalonia.Media private Matrix _currentTransform = Matrix.Identity; + private Matrix _currentContainerTransform = Matrix.Identity; + /// /// Gets the current transform of the drawing context. /// @@ -57,7 +59,7 @@ namespace Avalonia.Media } } - private Matrix _currentContainerTransform = Matrix.Identity; + internal Matrix CurrentContainerTransform => _currentContainerTransform; /// /// Draws a bitmap image. diff --git a/src/Avalonia.SceneGraph/Media/Geometry.cs b/src/Avalonia.SceneGraph/Media/Geometry.cs index e6eaaff042..092aea0c61 100644 --- a/src/Avalonia.SceneGraph/Media/Geometry.cs +++ b/src/Avalonia.SceneGraph/Media/Geometry.cs @@ -66,5 +66,15 @@ namespace Avalonia.Media { return PlatformImpl.GetRenderBounds(strokeThickness); } + + /// + /// Indicates whether the geometry contains the specified point. + /// + /// The point. + /// true if the geometry contains the point; otherwise, false. + public bool FillContains(Point point) + { + return PlatformImpl.FillContains(point); + } } } diff --git a/src/Avalonia.SceneGraph/Platform/IGeometryImpl.cs b/src/Avalonia.SceneGraph/Platform/IGeometryImpl.cs index f3a3f655e5..e18a76d739 100644 --- a/src/Avalonia.SceneGraph/Platform/IGeometryImpl.cs +++ b/src/Avalonia.SceneGraph/Platform/IGeometryImpl.cs @@ -24,5 +24,12 @@ namespace Avalonia.Platform /// The stroke thickness. /// The bounding rectangle. Rect GetRenderBounds(double strokeThickness); + + /// + /// Indicates whether the geometry contains the specified point. + /// + /// The point. + /// true if the geometry contains the point; otherwise, false. + bool FillContains(Point point); } } diff --git a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs index 3157feb369..a8899c7dcd 100644 --- a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs +++ b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs @@ -122,6 +122,12 @@ namespace Avalonia.Rendering using (context.PushTransformContainer()) { visual.Render(context); + var transformed = + new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform); + if (visual is Visual) + { + BoundsTracker.SetTransformedBounds((Visual)visual, transformed); + } var lst = GetSortedVisualList(visual.VisualChildren); diff --git a/src/Avalonia.SceneGraph/VisualTree/BoundsTracker.cs b/src/Avalonia.SceneGraph/VisualTree/BoundsTracker.cs index f39c0f0c87..4824cb141b 100644 --- a/src/Avalonia.SceneGraph/VisualTree/BoundsTracker.cs +++ b/src/Avalonia.SceneGraph/VisualTree/BoundsTracker.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; +using Avalonia.Media; namespace Avalonia.VisualTree { @@ -16,6 +17,9 @@ namespace Avalonia.VisualTree /// public class BoundsTracker { + private static AttachedProperty TransformedBoundsProperty = + AvaloniaProperty.RegisterAttached("TransformedBounds"); + /// /// Starts tracking the specified visual. /// @@ -23,42 +27,19 @@ namespace Avalonia.VisualTree /// An observable that returns the tracked bounds. public IObservable Track(Visual visual) { - return Track(visual, (Visual)visual.GetVisualRoot()); + return visual.GetObservable(TransformedBoundsProperty); } - /// - /// Starts tracking the specified visual relative to another control. - /// - /// The visual. - /// The control that the tracking should be relative to. - /// An observable that returns the tracked bounds. - public IObservable Track(Visual visual, Visual relativeTo) + internal static void SetTransformedBounds(Visual visual, TransformedBounds bounds) { - var visuals = visual.GetSelfAndVisualAncestors() - .TakeWhile(x => x != relativeTo) - .Reverse(); - var boundsSubscriptions = new List>(); - - foreach (var v in visuals.Cast()) - { - boundsSubscriptions.Add(v.GetObservable(Visual.BoundsProperty)); - } - - var bounds = boundsSubscriptions.CombineLatest().Select(ExtractBounds); - - // TODO: Track transform and clip rectangle. - return bounds.Select(x => new TransformedBounds(x, new Rect(), Matrix.Identity)); + visual.SetValue(TransformedBoundsProperty, bounds); } /// - /// Sums a collection of rectangles. + /// Gets the transformed bounds of the visual. /// - /// The collection of rectangles. - /// The summed rectangle. - private static Rect ExtractBounds(IList rects) - { - var position = rects.Select(x => x.Position).Aggregate((a, b) => a + b); - return new Rect(position, rects.Last().Size); - } + /// The visual. + /// The transformed bounds. + public static TransformedBounds GetTransformedBounds(Visual visual) => visual.GetValue(TransformedBoundsProperty); } } diff --git a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs index c7e0f3abe0..da8ed01ccc 100644 --- a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs @@ -1,15 +1,17 @@ // 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.Media; + namespace Avalonia.VisualTree { /// - /// Holds information about the bounds of a control, together with a transform and a clip/ + /// Holds information about the bounds of a control, together with a transform and a clip. /// - public class TransformedBounds + public struct TransformedBounds { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The control's bounds. /// The control's clip rectangle. @@ -35,5 +37,21 @@ namespace Avalonia.VisualTree /// Gets the control's transform. /// public Matrix Transform { get; } + + public Geometry GetTransformedBoundsGeometry() + { + StreamGeometry geometry = new StreamGeometry(); + using (var context = geometry.Open()) + { + context.SetFillRule(FillRule.EvenOdd); + context.BeginFigure(Bounds.TopLeft * Transform, true); + context.LineTo(Bounds.TopRight * Transform); + context.LineTo(Bounds.BottomRight * Transform); + context.LineTo(Bounds.BottomLeft * Transform); + context.LineTo(Bounds.TopLeft * Transform); + context.EndFigure(true); + } + return geometry; + } } } diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs index 126db894be..88d86182f8 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs @@ -62,6 +62,11 @@ namespace Avalonia.Cairo.Media } } + internal bool FillContains(Point point) + { + return _context.InFill(point.X, point.Y); + } + public void LineTo(Point point) { if (this.Path == null) diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs index 6152e73489..c325ebab14 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs @@ -67,5 +67,10 @@ namespace Avalonia.Cairo.Media { return _impl; } + + public bool FillContains(Point point) + { + return _impl.FillContains(point); + } } } diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 04b325ff81..071c45c477 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -75,6 +75,13 @@ namespace Avalonia.Skia return new StreamContext(this); } + public 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); + } + class StreamContext : IStreamGeometryContextImpl { private readonly StreamGeometryImpl _geometryImpl; diff --git a/src/Skia/Avalonia.Skia/readme.md b/src/Skia/Avalonia.Skia/readme.md index eec5df79f1..3defe9c58c 100644 --- a/src/Skia/Avalonia.Skia/readme.md +++ b/src/Skia/Avalonia.Skia/readme.md @@ -4,6 +4,9 @@ BitmapImpl - constructor from Width/Height - Save +StreamGeometryImpl +- Hit testing in Geometry missing as SkiaSharp does not expose this + DrawingContextImpl - Alpha support missing as SkiaSharp does not expose this - Gradient Shader caching? diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index 4c34c031c4..b98c944c7d 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -84,5 +84,12 @@ namespace Avalonia.Direct2D1.Media return DefiningGeometry.GetWidenedBounds((float)strokeThickness).ToAvalonia(); } } + + + public bool FillContains(Point point) + { + return Geometry.FillContainsPoint(point.ToSharpDX()); + } + } } diff --git a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj index a7144c7246..37ff49650a 100644 --- a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj +++ b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj @@ -1,4 +1,4 @@ - + @@ -35,6 +35,10 @@ 4 + + ..\..\packages\Moq.4.2.1510.2205\lib\net40\Moq.dll + True + diff --git a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs b/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs index 2f984ddce7..bbec9e239f 100644 --- a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs +++ b/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs @@ -1,9 +1,17 @@ // 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.Controls; using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.UnitTests; +using Moq; using Xunit; +using System.Collections.Generic; +using System.IO; namespace Avalonia.Input.UnitTests { @@ -12,59 +20,73 @@ namespace Avalonia.Input.UnitTests [Fact] public void InputHitTest_Should_Find_Control_At_Point() { - var container = new Decorator + using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) { - Width = 200, - Height = 200, - Child = new Border + var container = new Decorator { - Width = 100, - Height = 100, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - } - }; + Width = 200, + Height = 200, + Child = new Border + { + Width = 100, + Height = 100, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); - var result = container.InputHitTest(new Point(100, 100)); + var context = new DrawingContext(Mock.Of()); + context.Render(container); - Assert.Equal(container.Child, result); + var result = container.InputHitTest(new Point(100, 100)); + + Assert.Equal(container.Child, result); + } } [Fact] public void InputHitTest_Should_Not_Find_Control_Outside_Point() { - var container = new Decorator + using (UnitTestApplication.Start(new TestServices(renderInterface:new MockRenderInterface()))) { - Width = 200, - Height = 200, - Child = new Border + var container = new Decorator { - Width = 100, - Height = 100, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - } - }; + Width = 200, + Height = 200, + Child = new Border + { + Width = 100, + Height = 100, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); + var context = new DrawingContext(Mock.Of()); + context.Render(container); - var result = container.InputHitTest(new Point(10, 10)); + var result = container.InputHitTest(new Point(10, 10)); - Assert.Equal(container, result); + Assert.Equal(container, result); + } } [Fact] public void InputHitTest_Should_Find_Top_Control_At_Point() { - var container = new Panel + using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) { - Width = 200, - Height = 200, - Children = new Controls.Controls + var container = new Panel + { + Width = 200, + Height = 200, + Children = new Controls.Controls { new Border { @@ -81,24 +103,30 @@ namespace Avalonia.Input.UnitTests VerticalAlignment = VerticalAlignment.Center } } - }; + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); + var context = new DrawingContext(Mock.Of()); + context.Render(container); - var result = container.InputHitTest(new Point(100, 100)); + var result = container.InputHitTest(new Point(100, 100)); - Assert.Equal(container.Children[1], result); + Assert.Equal(container.Children[1], result); + } } [Fact] public void InputHitTest_Should_Find_Top_Control_At_Point_With_ZOrder() { - var container = new Panel + using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) { - Width = 200, - Height = 200, - Children = new Controls.Controls + var container = new Panel + { + Width = 200, + Height = 200, + Children = new Controls.Controls { new Border { @@ -116,14 +144,210 @@ namespace Avalonia.Input.UnitTests VerticalAlignment = VerticalAlignment.Center } } - }; + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); + + var context = new DrawingContext(Mock.Of()); + context.Render(container); + + var result = container.InputHitTest(new Point(100, 100)); + + Assert.Equal(container.Children[0], result); + } + } + + [Fact] + public void InputHitTest_Should_Find_Control_Translated_Outside_Parent_Bounds() + { + using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + { + Border target; + var container = new Panel + { + Width = 200, + Height = 200, + Children = new Controls.Controls + { + new Border + { + Width = 100, + Height = 100, + ZIndex = 1, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + Child = target = new Border + { + Width = 50, + Height = 50, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + RenderTransform = new TranslateTransform(110, 110), + } + }, + } + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); + + var context = new DrawingContext(Mock.Of()); + context.Render(container); + + var result = container.InputHitTest(new Point(120, 120)); + + Assert.Equal(target, result); + } + } + + + class MockRenderInterface : IPlatformRenderInterface + { + public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping) + { + throw new NotImplementedException(); + } + + public IRenderTarget CreateRenderer(IPlatformHandle handle) + { + throw new NotImplementedException(); + } + + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height) + { + throw new NotImplementedException(); + } + + public IStreamGeometryImpl CreateStreamGeometry() + { + return new MockStreamGeometry(); + } + + public IBitmapImpl LoadBitmap(Stream stream) + { + throw new NotImplementedException(); + } + + public IBitmapImpl LoadBitmap(string fileName) + { + throw new NotImplementedException(); + } + + class MockStreamGeometry : Avalonia.Platform.IStreamGeometryImpl + { + private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); + public Rect Bounds + { + get + { + throw new NotImplementedException(); + } + } + + public Matrix Transform + { + get + { + throw new NotImplementedException(); + } - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); + set + { + throw new NotImplementedException(); + } + } - var result = container.InputHitTest(new Point(100, 100)); + public IStreamGeometryImpl Clone() + { + return this; + } - Assert.Equal(container.Children[0], result); + public bool FillContains(Point point) + { + return _impl.FillContains(point); + } + + public Rect GetRenderBounds(double strokeThickness) + { + throw new NotImplementedException(); + } + + public IStreamGeometryContextImpl Open() + { + return _impl; + } + + class MockStreamGeometryContext : IStreamGeometryContextImpl + { + private List points = new List(); + public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + { + throw new NotImplementedException(); + } + + public void BeginFigure(Point startPoint, bool isFilled) + { + points.Add(startPoint); + } + + public void CubicBezierTo(Point point1, Point point2, Point point3) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + } + + public void EndFigure(bool isClosed) + { + } + + public void LineTo(Point point) + { + points.Add(point); + } + + public void QuadraticBezierTo(Point control, Point endPoint) + { + throw new NotImplementedException(); + } + + public void SetFillRule(FillRule fillRule) + { + } + + public bool FillContains(Point point) + { + // Use the algorithm from http://www.blackpawn.com/texts/pointinpoly/default.html + // to determine if the point is in the geometry (since it will always be convex in this situation) + for (int i = 0; i < points.Count; i++) + { + var a = points[i]; + var b = points[(i + 1) % points.Count]; + var c = points[(i + 2) % points.Count]; + + Vector v0 = c - a; + Vector v1 = b - a; + Vector v2 = point - a; + + var dot00 = v0 * v0; + var dot01 = v0 * v1; + var dot02 = v0 * v2; + var dot11 = v1 * v1; + var dot12 = v1 * v2; + + + var invDenom = 1 / (dot00 * dot11 - dot01 * dot01); + var u = (dot11 * dot02 - dot01 * dot12) * invDenom; + var v = (dot00 * dot12 - dot01 * dot02) * invDenom; + if ((u >= 0) && (v >= 0) && (u + v < 1)) return true; + } + return false; + } + } + } } } } diff --git a/tests/Avalonia.Input.UnitTests/packages.config b/tests/Avalonia.Input.UnitTests/packages.config index dcbf45edf0..fb397c586c 100644 --- a/tests/Avalonia.Input.UnitTests/packages.config +++ b/tests/Avalonia.Input.UnitTests/packages.config @@ -1,5 +1,6 @@  + diff --git a/tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj b/tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj index 46eae92c4d..91fd14fc47 100644 --- a/tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj +++ b/tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj @@ -1,4 +1,4 @@ - + @@ -127,6 +127,10 @@ {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F} Avalonia.Styling + + {88060192-33D5-4932-B0F9-8BD2763E857D} + Avalonia.UnitTests + diff --git a/tests/Avalonia.SceneGraph.UnitTests/VisualTree/BoundsTrackerTests.cs b/tests/Avalonia.SceneGraph.UnitTests/VisualTree/BoundsTrackerTests.cs index c4f3611f26..75eb9c3a50 100644 --- a/tests/Avalonia.SceneGraph.UnitTests/VisualTree/BoundsTrackerTests.cs +++ b/tests/Avalonia.SceneGraph.UnitTests/VisualTree/BoundsTrackerTests.cs @@ -8,7 +8,11 @@ using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Controls.Shapes; using Avalonia.VisualTree; +using Avalonia.Rendering; using Xunit; +using Avalonia.Media; +using Moq; +using Avalonia.UnitTests; namespace Avalonia.SceneGraph.UnitTests.VisualTree { @@ -17,36 +21,37 @@ namespace Avalonia.SceneGraph.UnitTests.VisualTree [Fact] public void Should_Track_Bounds() { - var target = new BoundsTracker(); - var control = default(Rectangle); - var tree = new Decorator + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Padding = new Thickness(10), - Child = new Decorator + var target = new BoundsTracker(); + var control = default(Rectangle); + var tree = new Decorator { - Padding = new Thickness(5), - Child = control = new Rectangle + Padding = new Thickness(10), + Child = new Decorator { - Width = 15, - Height = 15, - }, - } - }; + Padding = new Thickness(5), + Child = control = new Rectangle + { + Width = 15, + Height = 15, + }, + } + }; - tree.Measure(Size.Infinity); - tree.Arrange(new Rect(0, 0, 100, 100)); + var context = new DrawingContext(Mock.Of()); - var track = target.Track(control, tree); - var results = new List(); - track.Subscribe(results.Add); + tree.Measure(Size.Infinity); + tree.Arrange(new Rect(0, 0, 100, 100)); + context.Render(tree); - Assert.Equal(new Rect(42, 42, 15, 15), results[0].Bounds); + var track = target.Track(control); + var results = new List(); + track.Subscribe(results.Add); - tree.Padding = new Thickness(15); - tree.Measure(Size.Infinity); - tree.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(new Rect(37, 37, 15, 15), results[1].Bounds); + Assert.Equal(new Rect(0, 0, 15, 15), results[0].Bounds); + Assert.Equal(Matrix.CreateTranslation(42, 42), results[0].Transform); + } } } } From 796950443179cba73af3a1246ac27c6f05bc5157 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 22 May 2016 22:03:33 -0400 Subject: [PATCH 003/122] Added path clipping infrastructure. Got Skia implementation working. --- .../Media/DrawingContext.cs | 21 ++++++++++++--- .../Media/IDrawingContext.cs | 10 +++++-- .../Media/PathMarkupParser.cs | 27 +++++++++++++++++-- .../Rendering/RendererMixin.cs | 1 + src/Avalonia.SceneGraph/Visual.cs | 15 +++++++++++ src/Avalonia.SceneGraph/VisualTree/IVisual.cs | 5 ++++ src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 11 ++++++++ 7 files changed, 83 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.SceneGraph/Media/DrawingContext.cs b/src/Avalonia.SceneGraph/Media/DrawingContext.cs index 90a9749e2a..da33347215 100644 --- a/src/Avalonia.SceneGraph/Media/DrawingContext.cs +++ b/src/Avalonia.SceneGraph/Media/DrawingContext.cs @@ -127,7 +127,8 @@ namespace Avalonia.Media Matrix, Opacity, Clip, - MatrixContainer + MatrixContainer, + GeometryClip } public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default(Matrix)) @@ -149,10 +150,12 @@ namespace Avalonia.Media _context._states.Pop(); if (_type == PushedStateType.Matrix) _context.CurrentTransform = _matrix; - else if(_type == PushedStateType.Clip) + else if (_type == PushedStateType.Clip) _context._impl.PopClip(); - else if(_type == PushedStateType.Opacity) + else if (_type == PushedStateType.Opacity) _context._impl.PopOpacity(); + else if (_type == PushedStateType.GeometryClip) + _context._impl.PopGeometryClip(); else if (_type == PushedStateType.MatrixContainer) { var cont = _context._transformContainers.Pop(); @@ -174,6 +177,18 @@ namespace Avalonia.Media return new PushedState(this, PushedState.PushedStateType.Clip); } + /// + /// Pushes a clip geometry. + /// + /// The clip geometry. + /// A disposable used to undo the clip geometry. + public PushedState PushGeometryClip(Geometry clip) + { + Contract.Requires(clip != null); + _impl.PushGeometryClip(clip); + return new PushedState(this, PushedState.PushedStateType.GeometryClip); + } + /// /// Pushes an opacity value. /// diff --git a/src/Avalonia.SceneGraph/Media/IDrawingContext.cs b/src/Avalonia.SceneGraph/Media/IDrawingContext.cs index 31f5b0d38a..a798726cca 100644 --- a/src/Avalonia.SceneGraph/Media/IDrawingContext.cs +++ b/src/Avalonia.SceneGraph/Media/IDrawingContext.cs @@ -69,7 +69,6 @@ namespace Avalonia.Media /// Pushes a clip rectange. /// /// The clip rectangle. - /// A disposable used to undo the clip rectangle. void PushClip(Rect clip); void PopClip(); @@ -78,9 +77,16 @@ namespace Avalonia.Media /// Pushes an opacity value. /// /// The opacity. - /// A disposable used to undo the opacity. void PushOpacity(double opacity); void PopOpacity(); + + /// + /// Pushes a clip geometry. + /// + /// The clip geometry. + void PushGeometryClip(Geometry clip); + + void PopGeometryClip(); } } diff --git a/src/Avalonia.SceneGraph/Media/PathMarkupParser.cs b/src/Avalonia.SceneGraph/Media/PathMarkupParser.cs index a089d71127..70f999c860 100644 --- a/src/Avalonia.SceneGraph/Media/PathMarkupParser.cs +++ b/src/Avalonia.SceneGraph/Media/PathMarkupParser.cs @@ -34,6 +34,12 @@ namespace Avalonia.Media { 'z', Command.Close }, }; + private static readonly Dictionary FillRules = new Dictionary + { + {'0', FillRule.EvenOdd }, + {'1', FillRule.NonZero } + }; + private StreamGeometry _geometry; private readonly StreamGeometryContext _context; @@ -90,8 +96,7 @@ namespace Avalonia.Media switch (command) { case Command.FillRule: - // TODO: Implement. - reader.Read(); + _context.SetFillRule(ReadFillRule(reader)); break; case Command.Move: @@ -226,6 +231,24 @@ namespace Avalonia.Media } } + private static FillRule ReadFillRule(StringReader reader) + { + int i = reader.Read(); + if (i == -1) + { + throw new InvalidDataException("Invalid fill rule"); + } + char c = (char)i; + FillRule rule; + + if (!FillRules.TryGetValue(c, out rule)) + { + throw new InvalidDataException("Invalid fill rule"); + } + + return rule; + } + private static double ReadDouble(StringReader reader) { ReadWhitespace(reader); diff --git a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs index a8899c7dcd..b26d825064 100644 --- a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs +++ b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs @@ -119,6 +119,7 @@ namespace Avalonia.Rendering using (context.PushPostTransform(m)) using (context.PushOpacity(opacity)) using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState)) + using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState)) using (context.PushTransformContainer()) { visual.Render(context); diff --git a/src/Avalonia.SceneGraph/Visual.cs b/src/Avalonia.SceneGraph/Visual.cs index 9cf97c2767..8bdbfecb4a 100644 --- a/src/Avalonia.SceneGraph/Visual.cs +++ b/src/Avalonia.SceneGraph/Visual.cs @@ -39,6 +39,12 @@ namespace Avalonia public static readonly StyledProperty ClipToBoundsProperty = AvaloniaProperty.Register(nameof(ClipToBounds)); + /// + /// Defines the property. + /// + public static readonly StyledProperty ClipProperty = + AvaloniaProperty.Register(nameof(Clip)); + /// /// Defines the property. /// @@ -127,6 +133,15 @@ namespace Avalonia set { SetValue(ClipToBoundsProperty, value); } } + /// + /// Gets or sets the geometry clip for this visual. + /// + public Geometry Clip + { + get { return GetValue(ClipProperty); } + set { SetValue(ClipProperty, value); } + } + /// /// Gets a value indicating whether this scene graph node and all its parents are visible. /// diff --git a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs index 4d6a33b99c..fb4416f78e 100644 --- a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs +++ b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs @@ -41,6 +41,11 @@ namespace Avalonia.VisualTree /// bool ClipToBounds { get; set; } + /// + /// Gets or sets the geometry clip for this visual. + /// + Geometry Clip { get; set; } + /// /// Gets a value indicating whether this scene graph node is attached to a visual root. /// diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 6abe2ac971..00b54e8c5a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -288,6 +288,17 @@ namespace Avalonia.Skia { } + public void PushGeometryClip(Geometry clip) + { + Canvas.Save(); + Canvas.ClipPath(((StreamGeometryImpl)clip.PlatformImpl).EffectivePath); + } + + public void PopGeometryClip() + { + Canvas.Restore(); + } + private Matrix _currentTransform = Matrix.Identity; public Matrix Transform From 8f96486b8627f536510a3156f4e54fa12c7e0a83 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 23 May 2016 23:33:20 -0400 Subject: [PATCH 004/122] Added support for path/geometry clipping and fix bug in Cairo StreamGeometryContextImpl.FillContains. --- .../VisualTree/TransformedBounds.cs | 2 +- .../Avalonia.Cairo/Media/DrawingContext.cs | 12 +++++++++ .../Media/StreamGeometryContextImpl.cs | 6 ++++- .../Media/DrawingContext.cs | 26 +++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs index da8ed01ccc..1e8f484d75 100644 --- a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs @@ -49,7 +49,7 @@ namespace Avalonia.VisualTree context.LineTo(Bounds.BottomRight * Transform); context.LineTo(Bounds.BottomLeft * Transform); context.LineTo(Bounds.TopLeft * Transform); - context.EndFigure(true); + context.EndFigure(false); } return geometry; } diff --git a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs index 74e1c17cf0..07b544ad3b 100644 --- a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs @@ -340,5 +340,17 @@ namespace Avalonia.Cairo.Media return SetBrush(pen.Brush, destinationSize); } + + public void PushGeometryClip(Geometry clip) + { + _context.Save(); + _context.AppendPath(((StreamGeometryImpl)clip.PlatformImpl).Path); + _context.Clip(); + } + + public void PopGeometryClip() + { + _context.Restore(); + } } } diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs index 88d86182f8..1938d7e49e 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs @@ -64,7 +64,11 @@ namespace Avalonia.Cairo.Media internal bool FillContains(Point point) { - return _context.InFill(point.X, point.Y); + using (var context = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.Argb32, 0, 0))) + { + context.AppendPath(Path); + return context.InFill(point.X, point.Y); + } } public void LineTo(Point point) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs index add11e3cff..699a5ba18b 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs @@ -276,6 +276,11 @@ namespace Avalonia.Direct2D1.Media } public void PopOpacity() + { + PopLayer(); + } + + private void PopLayer() { var layer = _layers.Pop(); if (layer != null) @@ -324,5 +329,26 @@ namespace Avalonia.Direct2D1.Media return new SolidColorBrushImpl((Avalonia.Media.SolidColorBrush)null, _renderTarget); } } + + public void PushGeometryClip(Avalonia.Media.Geometry clip) + { + var parameters = new LayerParameters + { + ContentBounds = PrimitiveExtensions.RectangleInfinite, + MaskTransform = PrimitiveExtensions.Matrix3x2Identity, + Opacity = 1, + GeometricMask = ((GeometryImpl)clip.PlatformImpl).Geometry + }; + var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_renderTarget); + _renderTarget.PushLayer(ref parameters, layer); + + _layers.Push(layer); + + } + + public void PopGeometryClip() + { + PopLayer(); + } } } From eca0527f288a8a0c1d1edd8688036c3af0ed93ba Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 24 May 2016 21:38:08 -0400 Subject: [PATCH 005/122] Refactored render tests to use a shared project since they share source files. --- Avalonia.sln | 13 +++++++--- .../Avalonia.Cairo.RenderTests.csproj | 18 ++----------- .../Avalonia.Direct2D1.RenderTests.csproj | 17 +----------- .../Avalonia.RenderTests.projitems | 26 +++++++++++++++++++ .../Avalonia.RenderTests.shproj | 16 ++++++++++++ .../Avalonia.Skia.RenderTests.csproj | 17 ++---------- 6 files changed, 57 insertions(+), 50 deletions(-) create mode 100644 tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems create mode 100644 tests/Avalonia.RenderTests/Avalonia.RenderTests.shproj diff --git a/Avalonia.sln b/Avalonia.sln index 9af5841a0b..3c2e8fc83d 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -156,28 +156,34 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Avalonia.RenderTests", "tests\Avalonia.RenderTests\Avalonia.RenderTests.shproj", "{48840EDD-24BF-495D-911E-2EB12AE75D3B}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 + tests\Avalonia.RenderTests\Avalonia.RenderTests.projitems*{e106cf37-4066-4615-b684-172a6d30b058}*SharedItemsImports = 4 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13 + tests\Avalonia.RenderTests\Avalonia.RenderTests.projitems*{dabfd304-d6a4-4752-8123-c2ccf7ac7831}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{db070a10-bf39-4752-8456-86e9d5928478}*SharedItemsImports = 4 - src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4 + src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{78345174-5b52-4a14-b9fd-d5f2428137f0}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{54f237d5-a70a-4752-9656-0c70b1a7b047}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{ff69b927-c545-49ae-8e16-3d14d621aa12}*SharedItemsImports = 4 + tests\Avalonia.RenderTests\Avalonia.RenderTests.projitems*{d35a9f3d-8bb0-496e-bf72-444038a7debb}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{811a76cf-1cf6-440f-963b-bbe31bd72a82}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 4 - src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 + src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{8c923867-8a8f-4f6b-8b80-47d9e8436166}*SharedItemsImports = 4 + tests\Avalonia.RenderTests\Avalonia.RenderTests.projitems*{48840edd-24bf-495d-911e-2eb12ae75d3b}*SharedItemsImports = 13 samples\TestApplicationShared\TestApplicationShared.projitems*{e3a1060b-50d0-44e8-88b6-f44ef2e5bd72}*SharedItemsImports = 4 - src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 + src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{e1aa3dbf-9056-4530-9376-18119a7a3ffe}*SharedItemsImports = 4 EndGlobalSection @@ -1953,5 +1959,6 @@ Global {D35A9F3D-8BB0-496E-BF72-444038A7DEBB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {52F55355-D120-42AC-8116-8410A7D602FA} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {F1381F98-4D24-409A-A6C5-1C5B1E08BB08} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {48840EDD-24BF-495D-911E-2EB12AE75D3B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection EndGlobal diff --git a/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.csproj b/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.csproj index e9937dc566..6a7020334a 100644 --- a/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.csproj +++ b/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.csproj @@ -1,4 +1,4 @@ - + @@ -68,21 +68,6 @@ - - - - - - - - - - - - - - - {FB05AC90-89BA-4F2F-A924-F37875FB547C} @@ -128,6 +113,7 @@ + diff --git a/tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.csproj b/tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.csproj index fbc639e335..b81ff06e87 100644 --- a/tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.csproj +++ b/tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.csproj @@ -72,22 +72,6 @@ - - - - - - - - - - - - - - - - {D211E587-D8BC-45B9-95A4-F297C8FA5200} @@ -133,6 +117,7 @@ + diff --git a/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems b/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems new file mode 100644 index 0000000000..52f6a93b11 --- /dev/null +++ b/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems @@ -0,0 +1,26 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 48840edd-24bf-495d-911e-2eb12ae75d3b + + + Avalonia.RenderTests + + + + + + + + + + + + + + + + + diff --git a/tests/Avalonia.RenderTests/Avalonia.RenderTests.shproj b/tests/Avalonia.RenderTests/Avalonia.RenderTests.shproj new file mode 100644 index 0000000000..e3bed80491 --- /dev/null +++ b/tests/Avalonia.RenderTests/Avalonia.RenderTests.shproj @@ -0,0 +1,16 @@ + + + + 48840edd-24bf-495d-911e-2eb12ae75d3b + 14.0 + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/Avalonia.Skia.RenderTests.csproj b/tests/Avalonia.RenderTests/Avalonia.Skia.RenderTests.csproj index c649d58139..579e84874c 100644 --- a/tests/Avalonia.RenderTests/Avalonia.Skia.RenderTests.csproj +++ b/tests/Avalonia.RenderTests/Avalonia.Skia.RenderTests.csproj @@ -1,4 +1,4 @@ - + @@ -68,20 +68,6 @@ - - - - - - - - - - - - - - {d211e587-d8bc-45b9-95a4-f297c8fa5200} @@ -126,6 +112,7 @@ + From 33bd33e38c3c601dd02cb7c6a9c459e737128b55 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 24 May 2016 22:18:29 -0400 Subject: [PATCH 006/122] Added render tests for geometry clipping. --- .../Avalonia.RenderTests.projitems | 5 +- .../GeometryClippingTests.cs | 51 ++++++++++++++++++ .../Geometry_Clip_Clips_Path.expected.png | Bin 0 -> 373 bytes .../Geometry_Clip_Clips_Path.expected.png | Bin 0 -> 373 bytes .../Geometry_Clip_Clips_Path.expected.png | Bin 0 -> 373 bytes 5 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 tests/Avalonia.RenderTests/GeometryClippingTests.cs create mode 100644 tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png create mode 100644 tests/TestFiles/Direct2D1/GeometryClipping/Geometry_Clip_Clips_Path.expected.png create mode 100644 tests/TestFiles/Skia/GeometryClipping/Geometry_Clip_Clips_Path.expected.png diff --git a/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems b/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems index 52f6a93b11..79ceddf3bb 100644 --- a/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems +++ b/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems @@ -8,7 +8,7 @@ Avalonia.RenderTests - + @@ -22,5 +22,6 @@ + - + \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/GeometryClippingTests.cs b/tests/Avalonia.RenderTests/GeometryClippingTests.cs new file mode 100644 index 0000000000..355606bb43 --- /dev/null +++ b/tests/Avalonia.RenderTests/GeometryClippingTests.cs @@ -0,0 +1,51 @@ +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +#if AVALONIA_CAIRO +namespace Avalonia.Cairo.RenderTests +#elif AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests +#endif +{ + public class GeometryClippingTests : TestBase + { + public GeometryClippingTests() + :base("GeometryClipping") + { + } + + [Fact] + public void Geometry_Clip_Clips_Path() + { + var target = new Canvas + { + Clip = StreamGeometry.Parse("F1 M 0,0 H 76 V 76 Z"), + Width = 76, + Height = 76, + Children = new Avalonia.Controls.Controls + { + new Path + { + Width = 32, + Height = 40, + [Canvas.LeftProperty] = 23, + [Canvas.TopProperty] = 18, + Stretch = Stretch.Fill, + Fill = Brushes.Black, + Data = StreamGeometry.Parse("F1 M 27,18L 23,26L 33,30L 24,38L 33,46L 23,50L 27,58L 45,58L 55,38L 45,18L 27,18 Z") + } + } + }; + + RenderToFile(target); + CompareImages(); + } + } +} diff --git a/tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png b/tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..892899507b7d71439ee220baf272495473ad7394 GIT binary patch literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^J|N7&1|*M957Y)yjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4P2fsjv*CsZ?7KYWisS(xv1MXNu%}L0%48D zpcf2poR?&n>PQRo-T3eMze^;$0b~pad}!~xRvh#%@@1^<*B>7LB3GV`3D^F5DsA2B zsb#7ABf_OjWJ@N08)OB+01hCjPAggtUuaeVlk|&6$oM#C}o8B1w=Odoc5IU^YPcwGhT#F2MKt(`njxg HN@xNA&3~GM literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Direct2D1/GeometryClipping/Geometry_Clip_Clips_Path.expected.png b/tests/TestFiles/Direct2D1/GeometryClipping/Geometry_Clip_Clips_Path.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..892899507b7d71439ee220baf272495473ad7394 GIT binary patch literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^J|N7&1|*M957Y)yjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4P2fsjv*CsZ?7KYWisS(xv1MXNu%}L0%48D zpcf2poR?&n>PQRo-T3eMze^;$0b~pad}!~xRvh#%@@1^<*B>7LB3GV`3D^F5DsA2B zsb#7ABf_OjWJ@N08)OB+01hCjPAggtUuaeVlk|&6$oM#C}o8B1w=Odoc5IU^YPcwGhT#F2MKt(`njxg HN@xNA&3~GM literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/GeometryClipping/Geometry_Clip_Clips_Path.expected.png b/tests/TestFiles/Skia/GeometryClipping/Geometry_Clip_Clips_Path.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..892899507b7d71439ee220baf272495473ad7394 GIT binary patch literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^J|N7&1|*M957Y)yjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4P2fsjv*CsZ?7KYWisS(xv1MXNu%}L0%48D zpcf2poR?&n>PQRo-T3eMze^;$0b~pad}!~xRvh#%@@1^<*B>7LB3GV`3D^F5DsA2B zsb#7ABf_OjWJ@N08)OB+01hCjPAggtUuaeVlk|&6$oM#C}o8B1w=Odoc5IU^YPcwGhT#F2MKt(`njxg HN@xNA&3~GM literal 0 HcmV?d00001 From f75399a7b822462df4242f008d79d9bba86e37c8 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 24 May 2016 22:39:45 -0400 Subject: [PATCH 007/122] Minor fix in TransformedBounds. --- src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs index 1e8f484d75..da8ed01ccc 100644 --- a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs @@ -49,7 +49,7 @@ namespace Avalonia.VisualTree context.LineTo(Bounds.BottomRight * Transform); context.LineTo(Bounds.BottomLeft * Transform); context.LineTo(Bounds.TopLeft * Transform); - context.EndFigure(false); + context.EndFigure(true); } return geometry; } From 0c2060c07fdc76c9c7299d9dcc8ddc4a411e36b6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 May 2016 01:04:04 +0100 Subject: [PATCH 008/122] added horizontal and vertical offset properties to popup. --- src/Avalonia.Controls/Primitives/Popup.cs | 43 +++++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index c24d1cef2d..6bb4f31a56 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -39,6 +39,18 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty PlacementModeProperty = AvaloniaProperty.Register(nameof(PlacementMode), defaultValue: PlacementMode.Bottom); + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalOffsetProperty = + AvaloniaProperty.Register(nameof(HorizontalOffset)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalOffsetProperty = + AvaloniaProperty.Register(nameof(VerticalOffset)); + /// /// Defines the property. /// @@ -122,6 +134,24 @@ namespace Avalonia.Controls.Primitives set { SetValue(PlacementModeProperty, value); } } + /// + /// Gets or sets the Horizontal offset of the popup in relation to the + /// + public double HorizontalOffset + { + get { return GetValue(HorizontalOffsetProperty); } + set { SetValue(HorizontalOffsetProperty, value); } + } + + /// + /// Gets or sets the Vertical offset of the popup in relation to the + /// + public double VerticalOffset + { + get { return GetValue(VerticalOffsetProperty); } + set { SetValue(VerticalOffsetProperty, value); } + } + /// /// Gets or sets the control that is used to determine the popup's position. /// @@ -292,13 +322,20 @@ namespace Avalonia.Controls.Primitives switch (mode) { case PlacementMode.Pointer: - return MouseDevice.Instance?.Position ?? default(Point); + if (MouseDevice.Instance != null) + { + var offset = new Point(HorizontalOffset, VerticalOffset); + + return new Point(MouseDevice.Instance.Position.X + offset.X, MouseDevice.Instance.Position.Y + offset.Y); + } + + return default(Point); case PlacementMode.Bottom: - return target?.PointToScreen(new Point(0, target.Bounds.Height)) ?? zero; + return target?.PointToScreen(new Point(0 + HorizontalOffset, target.Bounds.Height + VerticalOffset)) ?? zero; case PlacementMode.Right: - return target?.PointToScreen(new Point(target.Bounds.Width, 0)) ?? zero; + return target?.PointToScreen(new Point(target.Bounds.Width + HorizontalOffset, 0 + VerticalOffset)) ?? zero; default: throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); From 3eada7eb202b3df24a8bc75f650c0af65268669a Mon Sep 17 00:00:00 2001 From: donandren Date: Fri, 27 May 2016 16:18:56 +0300 Subject: [PATCH 009/122] Fixes memory leak in hit testing simplifies hit testing not to use geometry, but simple transforms --- src/Avalonia.Input/InputExtensions.cs | 6 ++--- src/Avalonia.SceneGraph/Rect.cs | 4 ++-- .../VisualTree/TransformedBounds.cs | 22 +++++++++---------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index a2321f3493..5d80653322 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -25,7 +25,6 @@ namespace Avalonia.Input { Contract.Requires(element != null); var transformedBounds = BoundsTracker.GetTransformedBounds((Visual)element); - var geometry = transformedBounds.GetTransformedBoundsGeometry(); if (element.IsVisible && element.IsHitTestVisible && @@ -42,7 +41,7 @@ namespace Avalonia.Input } } - if (geometry.FillContains(p)) + if (transformedBounds.Contains(p)) { yield return element; } @@ -71,7 +70,6 @@ namespace Avalonia.Input }) .OrderBy(x => x, null) .Select(x => x.Element); - } private class ZOrderElement : IComparable @@ -95,4 +93,4 @@ namespace Avalonia.Input } } } -} +} \ No newline at end of file diff --git a/src/Avalonia.SceneGraph/Rect.cs b/src/Avalonia.SceneGraph/Rect.cs index de4d13890b..806ad39b53 100644 --- a/src/Avalonia.SceneGraph/Rect.cs +++ b/src/Avalonia.SceneGraph/Rect.cs @@ -230,8 +230,8 @@ namespace Avalonia /// true if the point is in the bounds of the rectangle; otherwise false. public bool Contains(Point p) { - return p.X >= _x && p.X < _x + _width && - p.Y >= _y && p.Y < _y + _height; + return p.X >= _x && p.X <= _x + _width && + p.Y >= _y && p.Y <= _y + _height; } /// diff --git a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs index da8ed01ccc..4c548669bd 100644 --- a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs @@ -38,20 +38,18 @@ namespace Avalonia.VisualTree /// public Matrix Transform { get; } - public Geometry GetTransformedBoundsGeometry() + public bool Contains(Point point) { - StreamGeometry geometry = new StreamGeometry(); - using (var context = geometry.Open()) + if (Transform.HasInverse) { - context.SetFillRule(FillRule.EvenOdd); - context.BeginFigure(Bounds.TopLeft * Transform, true); - context.LineTo(Bounds.TopRight * Transform); - context.LineTo(Bounds.BottomRight * Transform); - context.LineTo(Bounds.BottomLeft * Transform); - context.LineTo(Bounds.TopLeft * Transform); - context.EndFigure(true); + Point trPoint = point * Transform.Invert(); + + return Bounds.Contains(trPoint); + } + else + { + return Bounds.Contains(point); } - return geometry; } } -} +} \ No newline at end of file From 31147af9d30a0680b4f606f4f9cdc0936b24dfa8 Mon Sep 17 00:00:00 2001 From: donandren Date: Fri, 27 May 2016 16:50:28 +0300 Subject: [PATCH 010/122] renamed Visual.TransformOrigin -> RenderTransformOrigin --- src/Avalonia.Controls/LayoutTransformControl.cs | 2 +- src/Avalonia.Controls/Primitives/AdornerLayer.cs | 2 +- src/Avalonia.SceneGraph/Rendering/RendererMixin.cs | 4 ++-- src/Avalonia.SceneGraph/Visual.cs | 12 ++++++------ src/Avalonia.SceneGraph/VisualTree/IVisual.cs | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index ded75d708a..ed7a50d9b0 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -139,7 +139,7 @@ namespace Avalonia.Controls if (null != TransformRoot) { TransformRoot.RenderTransform = _matrixTransform; - TransformRoot.TransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute); + TransformRoot.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute); } ApplyLayoutTransform(); diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index b732387cf5..1dfa2d7af2 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -60,7 +60,7 @@ namespace Avalonia.Controls.Primitives if (info != null) { child.RenderTransform = new MatrixTransform(info.Bounds.Transform); - child.TransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute); + child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute); child.Arrange(info.Bounds.Bounds); } else diff --git a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs index b26d825064..fbd5550c95 100644 --- a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs +++ b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs @@ -104,7 +104,7 @@ namespace Avalonia.Rendering if (visual.RenderTransform != null) { - var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); + var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); var offset = Matrix.CreateTranslation(origin); renderTransform = (-offset) * visual.RenderTransform.Value * (offset); } @@ -171,7 +171,7 @@ namespace Avalonia.Rendering } else { - var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); + var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); var offset = Matrix.CreateTranslation(visual.Bounds.Position + origin); var m = (-offset) * visual.RenderTransform.Value * (offset); return visual.Bounds.TransformToAABB(m); diff --git a/src/Avalonia.SceneGraph/Visual.cs b/src/Avalonia.SceneGraph/Visual.cs index 8bdbfecb4a..95cae794af 100644 --- a/src/Avalonia.SceneGraph/Visual.cs +++ b/src/Avalonia.SceneGraph/Visual.cs @@ -64,10 +64,10 @@ namespace Avalonia AvaloniaProperty.Register(nameof(RenderTransform)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty TransformOriginProperty = - AvaloniaProperty.Register(nameof(TransformOrigin), defaultValue: RelativePoint.Center); + public static readonly StyledProperty RenderTransformOriginProperty = + AvaloniaProperty.Register(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center); /// /// Defines the property. @@ -180,10 +180,10 @@ namespace Avalonia /// /// Gets the transform origin of the scene graph node. /// - public RelativePoint TransformOrigin + public RelativePoint RenderTransformOrigin { - get { return GetValue(TransformOriginProperty); } - set { SetValue(TransformOriginProperty, value); } + get { return GetValue(RenderTransformOriginProperty); } + set { SetValue(RenderTransformOriginProperty, value); } } /// diff --git a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs index fb4416f78e..3a01ac0afc 100644 --- a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs +++ b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs @@ -72,9 +72,9 @@ namespace Avalonia.VisualTree Transform RenderTransform { get; set; } /// - /// Gets or sets the transform origin of the scene graph node. + /// Gets or sets the render transform origin of the scene graph node. /// - RelativePoint TransformOrigin { get; set; } + RelativePoint RenderTransformOrigin { get; set; } /// /// Gets the scene graph node's child nodes. From e931bcf79c30fe9345c8ca1d56123775ca318acb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 May 2016 22:41:10 +0100 Subject: [PATCH 011/122] correctly scale offsets for pointer placement mode. --- src/Avalonia.Controls/Primitives/Popup.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 6bb4f31a56..81d01ff74f 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -10,6 +10,7 @@ using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Rendering; using Avalonia.VisualTree; +using Avalonia.Layout; namespace Avalonia.Controls.Primitives { @@ -317,21 +318,22 @@ namespace Avalonia.Controls.Primitives if (target?.GetVisualRoot() == null) { mode = PlacementMode.Pointer; - } + } switch (mode) { case PlacementMode.Pointer: if (MouseDevice.Instance != null) { - var offset = new Point(HorizontalOffset, VerticalOffset); - - return new Point(MouseDevice.Instance.Position.X + offset.X, MouseDevice.Instance.Position.Y + offset.Y); + // Scales the Horizontal and Vertical offset to screen co-ordinates. + var screenOffset = new Point(HorizontalOffset * (PopupRoot as ILayoutRoot).LayoutScaling, VerticalOffset * (PopupRoot as ILayoutRoot).LayoutScaling); + return MouseDevice.Instance.Position + screenOffset; } return default(Point); case PlacementMode.Bottom: + return target?.PointToScreen(new Point(0 + HorizontalOffset, target.Bounds.Height + VerticalOffset)) ?? zero; case PlacementMode.Right: From 5e0f12235f5cf36afcec54562719df4beb737454 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 27 May 2016 22:24:46 -0400 Subject: [PATCH 012/122] Added the platform independent interface and the Direct2D1 implementation of opacity masks. --- .../Media/DrawingContext.cs | 16 ++++++++++++++- .../Media/IDrawingContext.cs | 4 ++++ .../Rendering/RendererMixin.cs | 1 + src/Avalonia.SceneGraph/Visual.cs | 16 +++++++++++++++ src/Avalonia.SceneGraph/VisualTree/IVisual.cs | 5 +++++ .../Media/DrawingContext.cs | 20 +++++++++++++++++++ 6 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.SceneGraph/Media/DrawingContext.cs b/src/Avalonia.SceneGraph/Media/DrawingContext.cs index da33347215..d56d2ae1ad 100644 --- a/src/Avalonia.SceneGraph/Media/DrawingContext.cs +++ b/src/Avalonia.SceneGraph/Media/DrawingContext.cs @@ -128,7 +128,8 @@ namespace Avalonia.Media Opacity, Clip, MatrixContainer, - GeometryClip + GeometryClip, + OpacityMask } public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default(Matrix)) @@ -156,6 +157,8 @@ namespace Avalonia.Media _context._impl.PopOpacity(); else if (_type == PushedStateType.GeometryClip) _context._impl.PopGeometryClip(); + else if (_type == PushedStateType.OpacityMask) + _context._impl.PopOpacityMask(); else if (_type == PushedStateType.MatrixContainer) { var cont = _context._transformContainers.Pop(); @@ -201,6 +204,17 @@ namespace Avalonia.Media return new PushedState(this, PushedState.PushedStateType.Opacity); } + /// + /// Pushes an opacity mask. + /// + /// The opacity mask. + /// A disposable to undo the opacity mask. + public PushedState PushOpacityMask(IBrush mask, Rect bounds) + { + _impl.PushOpacityMask(mask, bounds); + return new PushedState(this, PushedState.PushedStateType.OpacityMask); + } + /// /// Pushes a matrix transformation. /// diff --git a/src/Avalonia.SceneGraph/Media/IDrawingContext.cs b/src/Avalonia.SceneGraph/Media/IDrawingContext.cs index a798726cca..c63abc160f 100644 --- a/src/Avalonia.SceneGraph/Media/IDrawingContext.cs +++ b/src/Avalonia.SceneGraph/Media/IDrawingContext.cs @@ -81,6 +81,10 @@ namespace Avalonia.Media void PopOpacity(); + void PushOpacityMask(IBrush mask, Rect bounds); + + void PopOpacityMask(); + /// /// Pushes a clip geometry. /// diff --git a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs index b26d825064..1f784f0017 100644 --- a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs +++ b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs @@ -120,6 +120,7 @@ namespace Avalonia.Rendering using (context.PushOpacity(opacity)) using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState)) using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState)) + using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, clipToBounds ? bounds : clipRect) : default(DrawingContext.PushedState)) using (context.PushTransformContainer()) { visual.Render(context); diff --git a/src/Avalonia.SceneGraph/Visual.cs b/src/Avalonia.SceneGraph/Visual.cs index 8bdbfecb4a..82ce1b85c7 100644 --- a/src/Avalonia.SceneGraph/Visual.cs +++ b/src/Avalonia.SceneGraph/Visual.cs @@ -57,6 +57,12 @@ namespace Avalonia public static readonly StyledProperty OpacityProperty = AvaloniaProperty.Register(nameof(Opacity), 1); + /// + /// Defines the property. + /// + public static readonly StyledProperty OpacityMaskProperty = + AvaloniaProperty.Register(nameof(OpacityMask)); + /// /// Defines the property. /// @@ -168,6 +174,16 @@ namespace Avalonia set { SetValue(OpacityProperty, value); } } + + /// + /// Gets the opacity mask of the scene graph node. + /// + public IBrush OpacityMask + { + get { return GetValue(OpacityMaskProperty); } + set { SetValue(OpacityMaskProperty, value); } + } + /// /// Gets the render transform of the scene graph node. /// diff --git a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs index fb4416f78e..9b4a4fccf5 100644 --- a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs +++ b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs @@ -66,6 +66,11 @@ namespace Avalonia.VisualTree /// double Opacity { get; set; } + /// + /// Gets or sets the opacity mask of the scene graph node. + /// + IBrush OpacityMask { get; set; } + /// /// Gets or sets the render transform of the scene graph node. /// diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs index 699a5ba18b..e0c75b44e1 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs @@ -350,5 +350,25 @@ namespace Avalonia.Direct2D1.Media { PopLayer(); } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + var parameters = new LayerParameters + { + ContentBounds = PrimitiveExtensions.RectangleInfinite, + MaskTransform = PrimitiveExtensions.Matrix3x2Identity, + Opacity = 1, + OpacityBrush = CreateBrush(mask, bounds.Size).PlatformBrush + }; + var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_renderTarget); + _renderTarget.PushLayer(ref parameters, layer); + + _layers.Push(layer); + } + + public void PopOpacityMask() + { + PopLayer(); + } } } From f70bed953b44f49b83c9f8aff6b8a5665ac5de8c Mon Sep 17 00:00:00 2001 From: Stano Turza Date: Sat, 28 May 2016 10:11:04 +0200 Subject: [PATCH 013/122] Fix broken Gitter link --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 1c77a38c08..63047fdc85 100644 --- a/readme.md +++ b/readme.md @@ -45,7 +45,7 @@ framework. As mentioned above, Avalonia is still in alpha and as such there's not much documentation yet. You can take a look at the [getting started page](docs/tutorial/gettingstarted.md) for an overview of how to get started but probably the best thing to do for now is to already know a little bit -about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/Avalonia/Avalonia). +about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). There's also a high-level [architecture document](docs/spec/architecture.md) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/. From 180e3bf21d0ce6b3cba68a119e63ded839479ed3 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 28 May 2016 17:02:44 -0400 Subject: [PATCH 014/122] Added support for opacity masks into Cairo. --- .../Avalonia.Cairo/Media/DrawingContext.cs | 80 ++++++++++++------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs index 07b544ad3b..c2f703918d 100644 --- a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs @@ -15,7 +15,7 @@ namespace Avalonia.Cairo.Media using Cairo = global::Cairo; /// - /// Draws using Direct2D1. + /// Draws using Cairo. /// public class DrawingContext : IDrawingContextImpl, IDisposable { @@ -24,6 +24,8 @@ namespace Avalonia.Cairo.Media /// private readonly Cairo.Context _context; + private readonly Stack _maskStack = new Stack(); + /// /// Initializes a new instance of the class. /// @@ -273,46 +275,53 @@ namespace Avalonia.Cairo.Media private IDisposable SetBrush(IBrush brush, Size destinationSize) { - _context.Save (); + _context.Save(); + + BrushImpl impl = CreateBrushImpl(brush, destinationSize); + _context.SetSource(impl.PlatformBrush); + return Disposable.Create(() => + { + impl.Dispose(); + _context.Restore(); + }); + } + + private BrushImpl CreateBrushImpl(IBrush brush, Size destinationSize) + { var solid = brush as SolidColorBrush; var linearGradientBrush = brush as LinearGradientBrush; var radialGradientBrush = brush as RadialGradientBrush; var imageBrush = brush as ImageBrush; var visualBrush = brush as VisualBrush; - BrushImpl impl = null; + BrushImpl impl = null; - if (solid != null) - { - impl = new SolidColorBrushImpl(solid, opacityOverride); - } - else if (linearGradientBrush != null) - { - impl = new LinearGradientBrushImpl(linearGradientBrush, destinationSize); - } + if (solid != null) + { + impl = new SolidColorBrushImpl(solid, opacityOverride); + } + else if (linearGradientBrush != null) + { + impl = new LinearGradientBrushImpl(linearGradientBrush, destinationSize); + } else if (radialGradientBrush != null) { impl = new RadialGradientBrushImpl(radialGradientBrush, destinationSize); } - else if (imageBrush != null) - { - impl = new ImageBrushImpl(imageBrush, destinationSize); - } - else if (visualBrush != null) - { - impl = new VisualBrushImpl(visualBrush, destinationSize); - } - else - { - impl = new SolidColorBrushImpl(null, opacityOverride); - } + else if (imageBrush != null) + { + impl = new ImageBrushImpl(imageBrush, destinationSize); + } + else if (visualBrush != null) + { + impl = new VisualBrushImpl(visualBrush, destinationSize); + } + else + { + impl = new SolidColorBrushImpl(null, opacityOverride); + } - _context.SetSource(impl.PlatformBrush); - return Disposable.Create(() => - { - impl.Dispose(); - _context.Restore(); - }); + return impl; } private IDisposable SetPen(Pen pen, Size destinationSize) @@ -352,5 +361,18 @@ namespace Avalonia.Cairo.Media { _context.Restore(); } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + _context.PushGroup(); + var impl = CreateBrushImpl(mask, bounds.Size); + _maskStack.Push(impl); + } + + public void PopOpacityMask() + { + _context.PopGroupToSource(); + _context.Mask(_maskStack.Pop().PlatformBrush); + } } } From c6f2c4bd9c906ab879817b381fe80ea76bc53c83 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 28 May 2016 18:56:47 -0400 Subject: [PATCH 015/122] Added a do-nothing interface for opacity masks for Skia. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 00b54e8c5a..781ea4b4df 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -299,6 +299,16 @@ namespace Avalonia.Skia Canvas.Restore(); } + public void PushOpacityMask(IBrush mask, Rect bounds) + { + //TODO: Skia does not support opacity masks + } + + public void PopOpacityMask() + { + //TODO: Skia does not support opacity masks + } + private Matrix _currentTransform = Matrix.Identity; public Matrix Transform From 5bbdfc55b4398a8d748e6561991a7ed70f614e2a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 29 May 2016 12:26:48 -0400 Subject: [PATCH 016/122] Added a render test. --- .../Avalonia.RenderTests.projitems | 3 +- .../Avalonia.RenderTests/OpacityMaskTests.cs | 64 ++++++++++++++++++ .../Opacity_Mask_Masks_Element.expected.png | Bin 0 -> 846 bytes .../Opacity_Mask_Masks_Element.expected.png | Bin 0 -> 989 bytes .../Opacity_Mask_Masks_Element.expected.png | Bin 0 -> 846 bytes 5 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.RenderTests/OpacityMaskTests.cs create mode 100644 tests/TestFiles/Cairo/OpacityMask/Opacity_Mask_Masks_Element.expected.png create mode 100644 tests/TestFiles/Direct2D1/OpacityMask/Opacity_Mask_Masks_Element.expected.png create mode 100644 tests/TestFiles/Skia/OpacityMask/Opacity_Mask_Masks_Element.expected.png diff --git a/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems b/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems index 79ceddf3bb..0d54c021ca 100644 --- a/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems +++ b/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems @@ -9,6 +9,8 @@ Avalonia.RenderTests + + @@ -22,6 +24,5 @@ - \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/OpacityMaskTests.cs b/tests/Avalonia.RenderTests/OpacityMaskTests.cs new file mode 100644 index 0000000000..7d9b6d7fa8 --- /dev/null +++ b/tests/Avalonia.RenderTests/OpacityMaskTests.cs @@ -0,0 +1,64 @@ +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +#if AVALONIA_CAIRO +namespace Avalonia.Cairo.RenderTests +#elif AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests +#endif +{ + public class OpacityMaskTests : TestBase + { + public OpacityMaskTests() + : base("OpacityMask") + { + } + +#if AVALONIA_SKIA + [Fact(Skip = "Opacity Masks not supported on Skia")] +#else + [Fact] +#endif + public void Opacity_Mask_Masks_Element() + { + var target = new Canvas + { + OpacityMask = new LinearGradientBrush + { + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative), + GradientStops = new List + { + new GradientStop(Color.FromUInt32(0xffffffff), 0), + new GradientStop(Color.FromUInt32(0x00ffffff), 1) + } + }, + Width = 76, + Height = 76, + Children = new Avalonia.Controls.Controls + { + new Path + { + Width = 32, + Height = 40, + [Canvas.LeftProperty] = 23, + [Canvas.TopProperty] = 18, + Stretch = Stretch.Fill, + Fill = Brushes.Red, + Data = StreamGeometry.Parse("F1 M 27,18L 23,26L 33,30L 24,38L 33,46L 23,50L 27,58L 45,58L 55,38L 45,18L 27,18 Z") + } + } + }; + + RenderToFile(target); + CompareImages(); + } + } +} diff --git a/tests/TestFiles/Cairo/OpacityMask/Opacity_Mask_Masks_Element.expected.png b/tests/TestFiles/Cairo/OpacityMask/Opacity_Mask_Masks_Element.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..f57c675b7a013cb3fe4f94241527f96150ca10b6 GIT binary patch literal 846 zcmV-U1F`&xP)i%%vIn$gk44mb`ymP*L z=Du_93`J2CMNt$*Q4~c{6h%=K#WW0AH)?F)Rm}RIX8Xi@U3+cdMpo{-xh?Tt&pNoQ zw$HVv6Z2jmMOfn;9&&J5`*qpb#se9-FO$__S4<~sj4vnUzCgIJ#>0AdO(|=JGmW#+ z`7e{vVOLBqYmA>v$bW%QVe{_V5F=}b(~UEcH7pa|Vb6$@HO5Z_*RVjeu=jBle~OVc z!>Pvo!FAjsWpHgA!$mxfQ>K$O#y1Au$*3i5(jRG@#$(vTQLD=u<97$PqVrD|HZt$J zO{$Pw=(XWi^~^oQTeQZNeIN;01kRUscu^Uy9CqEy5ai z;YB=RI#~@TTK{;5kuI!p4_?+c&r`}8<3}U&PZc)*@4te3#mH(n9=VpK32QuvmvFZ@ zSz~-5s*bGluuXl210MM?T#i4Ds3)Vt3|w#gj^}Zwcv(LUY)90R_LLm>7O&%1v9f0P zEUJ#wIc(r2-oTIIWc?h~U94f!ANUP#;ycsJYWO@d|F!HT=O-0!;|tTu3R}BI)_J`1 z*T4t*5ziE|X1E@mcgj=p-)Q~b8&*F&9OLcz8W~Euu=$qdye{jAX=J5z7fHfepS{lM zqEn;)}W(0?j$)U*ry?)UmKvX0$Eci2<@xkgPbYiD35xQ<&yKP5jB zE34_FNgAQT2ENySDHtVdJF~mUnZvG%ku}3t3HdLOalEsozowRTbKsZg{Flj^^glMO zteO4=a5^c%27b_AhZXc+vUb+dUF7a1uQ^<2?RD*&#Jr25D2k#eilQirq9}@@D2fpN Y20PuSvP48Ip#T5?07*qoM6N<$g0Li&%m4rY literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Direct2D1/OpacityMask/Opacity_Mask_Masks_Element.expected.png b/tests/TestFiles/Direct2D1/OpacityMask/Opacity_Mask_Masks_Element.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..ba8312accfdc140ba2f09abbbc620f7e0f01247e GIT binary patch literal 989 zcmV<310wv1P)Px#1ZP1_K>z@;j|==^1poj8hekyLe-qG%`Oa@>|wY}WI_iZqQrex)yp z-c!xtTrhz9*Y>#WIxJD1OP*?u)r6fv)uwCu7Q;&W6UZF( zz;L#MvJEv2pv|yETexhcnqo0wr%}@YItVL$-f^las)6B5rYpU7H8S04SW7gcY>cut zLwmwxx|*5pCaiS-C^ADku$$-}27geiO@|0eG{RzpY69o$gP~REAnjqaIbc}DWN((Q z{4Lh%m;{yumvYZJgeK8IytN!kqrOR4zmvz@CbW-KRm=lR!SNtvk{56Qx0~Xv<$$t% z3t`FLJb~Szs-k@lYX}GbL{D)0fn`NN*}j#qWKA9BGE7yaXMy3g4}<4vkDyWzNiFxS zhLw5DexyoI0>jxL4lmM<#mgXJiH5igQPnUH3`1)uQ%G+9r67`8?mG-C?e`%yOasHw zdW>G8U5m0N%YBz&iE>-VD7O=79;A`h{-LzQJj* zuxQloT`4^YOc28yewX$+3K5p9Um;a`78nKx&|cbHps*G&vq)8i^K8MH{K~Q$$-m(G z1BE5J|K9|LDVe4*4jZSgi?@Qk4W%!j2lVQ2jvly?KeNc&ZWjmjV=&|mKKodiF8NVK z9qoJANi?AxR>3MPK|8QViD9jN|YK79hT{I3EC4TJVqK}iQXbp z)C1!H`ET?*F0)1(_N|7+{-X7C2^JG3--@47YD`!6t%fB%3-jp`tOEN)kx8pCo$cER z3(0hSrJ7?BSi&<@H-I+7lKKugj^j9v<2a7vIF92uj^jB0Fj4de;>96#otU{!00000 LNkvXXu0mjf#i+se literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/OpacityMask/Opacity_Mask_Masks_Element.expected.png b/tests/TestFiles/Skia/OpacityMask/Opacity_Mask_Masks_Element.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..f57c675b7a013cb3fe4f94241527f96150ca10b6 GIT binary patch literal 846 zcmV-U1F`&xP)i%%vIn$gk44mb`ymP*L z=Du_93`J2CMNt$*Q4~c{6h%=K#WW0AH)?F)Rm}RIX8Xi@U3+cdMpo{-xh?Tt&pNoQ zw$HVv6Z2jmMOfn;9&&J5`*qpb#se9-FO$__S4<~sj4vnUzCgIJ#>0AdO(|=JGmW#+ z`7e{vVOLBqYmA>v$bW%QVe{_V5F=}b(~UEcH7pa|Vb6$@HO5Z_*RVjeu=jBle~OVc z!>Pvo!FAjsWpHgA!$mxfQ>K$O#y1Au$*3i5(jRG@#$(vTQLD=u<97$PqVrD|HZt$J zO{$Pw=(XWi^~^oQTeQZNeIN;01kRUscu^Uy9CqEy5ai z;YB=RI#~@TTK{;5kuI!p4_?+c&r`}8<3}U&PZc)*@4te3#mH(n9=VpK32QuvmvFZ@ zSz~-5s*bGluuXl210MM?T#i4Ds3)Vt3|w#gj^}Zwcv(LUY)90R_LLm>7O&%1v9f0P zEUJ#wIc(r2-oTIIWc?h~U94f!ANUP#;ycsJYWO@d|F!HT=O-0!;|tTu3R}BI)_J`1 z*T4t*5ziE|X1E@mcgj=p-)Q~b8&*F&9OLcz8W~Euu=$qdye{jAX=J5z7fHfepS{lM zqEn;)}W(0?j$)U*ry?)UmKvX0$Eci2<@xkgPbYiD35xQ<&yKP5jB zE34_FNgAQT2ENySDHtVdJF~mUnZvG%ku}3t3HdLOalEsozowRTbKsZg{Flj^^glMO zteO4=a5^c%27b_AhZXc+vUb+dUF7a1uQ^<2?RD*&#Jr25D2k#eilQirq9}@@D2fpN Y20PuSvP48Ip#T5?07*qoM6N<$g0Li&%m4rY literal 0 HcmV?d00001 From 9c4ce4efdc94da12b4ef335aebc40490d6a57a1b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Jun 2016 16:51:25 +0200 Subject: [PATCH 017/122] Fix formatting. --- src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs b/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs index 5b84b0ddd3..3473c4a094 100644 --- a/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs @@ -55,7 +55,8 @@ namespace Avalonia.Media.Imaging /// public IBitmapImpl PlatformImpl { - get; } + get; + } /// /// Saves the bitmap to a file. From f660da41c73cca5a8852a9c383927a1958a7d4c7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Jun 2016 20:15:07 +0200 Subject: [PATCH 018/122] Fix virtualized list to taking up available space. Previous commit was slightly wrong: too many controls were being created for the list in VirtualizingTest. --- .../Avalonia.Controls.csproj | 1 + .../IVirtualizingController.cs | 22 +++++++++ src/Avalonia.Controls/IVirtualizingPanel.cs | 12 +++++ .../Presenters/ItemVirtualizer.cs | 45 ++++++++++++------- .../Presenters/ItemVirtualizerNone.cs | 6 --- .../Presenters/ItemVirtualizerSimple.cs | 2 +- .../Presenters/ItemsPresenter.cs | 8 ---- .../VirtualizingStackPanel.cs | 38 ++++++++++++---- .../ListBoxTests.cs | 2 +- .../ItemsPresenterTests_Virtualization.cs | 18 +++++++- .../VirtualizingStackPanelTests.cs | 32 ++++++++++++- 11 files changed, 141 insertions(+), 45 deletions(-) create mode 100644 src/Avalonia.Controls/IVirtualizingController.cs diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 22d17bf633..47a56fcc82 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -60,6 +60,7 @@ + diff --git a/src/Avalonia.Controls/IVirtualizingController.cs b/src/Avalonia.Controls/IVirtualizingController.cs new file mode 100644 index 0000000000..0b997f4948 --- /dev/null +++ b/src/Avalonia.Controls/IVirtualizingController.cs @@ -0,0 +1,22 @@ +// 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. + +namespace Avalonia.Controls +{ + /// + /// Interface implemented by controls that act as controllers for an + /// . + /// + public interface IVirtualizingController + { + /// + /// Called when the 's controls should be updated. + /// + /// + /// The controller should respond to this method being called by either adding + /// children up until becomes true or + /// removing controls. + /// + void UpdateControls(); + } +} diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index ce320c0da7..ca75517240 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Controls.Primitives; namespace Avalonia.Controls { @@ -10,6 +11,17 @@ namespace Avalonia.Controls /// public interface IVirtualizingPanel : IPanel { + /// + /// Gets or sets the controller for the virtualizing panel. + /// + /// + /// A virtualizing controller is responsible for maintaing the controls in the virtualizing + /// panel. This property will be set by the controller when virtualization is initialized. + /// Note that this property may remain null if the panel is added to a control that does + /// not act as a virtualizing controller. + /// + IVirtualizingController Controller { get; set; } + /// /// Gets a value indicating whether the panel is full. /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index dd7cf3bf1d..8f77cff32b 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.Presenters /// /// Base class for classes which handle virtualization for an . /// - internal abstract class ItemVirtualizer + internal abstract class ItemVirtualizer : IVirtualizingController { /// /// Initializes a new instance of the class. @@ -115,34 +115,34 @@ namespace Avalonia.Controls.Presenters { var virtualizingPanel = owner.Panel as IVirtualizingPanel; var scrollable = (ILogicalScrollable)owner; + ItemVirtualizer result = null; if (virtualizingPanel != null && scrollable.InvalidateScroll != null) { switch (owner.VirtualizationMode) { case ItemVirtualizationMode.Simple: - return new ItemVirtualizerSimple(owner); + result = new ItemVirtualizerSimple(owner); + break; } } - return new ItemVirtualizerNone(owner); - } + if (result == null) + { + result = new ItemVirtualizerNone(owner); + } - /// - /// Called by the when it carries out an arrange. - /// - /// The final size passed to the arrange. - public abstract void Arranging(Size finalSize); + if (virtualizingPanel != null) + { + virtualizingPanel.Controller = result; + } - /// - /// Called when a request is made to bring an item into view. - /// - /// The item to bring into view. - /// The rect on the item to bring into view. - /// True if the request was handled; otherwise false. - public virtual bool BringIntoView(IVisual target, Rect targetRect) + return result; + } + + /// + public virtual void UpdateControls() { - return false; } /// @@ -157,5 +157,16 @@ namespace Avalonia.Controls.Presenters Items = items; ItemCount = items.Count(); } + + /// + /// Called when a request is made to bring an item into view. + /// + /// The item to bring into view. + /// The rect on the item to bring into view. + /// True if the request was handled; otherwise false. + public virtual bool BringIntoView(IVisual target, Rect targetRect) + { + return false; + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 303c7442c2..7450fdb2b4 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -52,12 +52,6 @@ namespace Avalonia.Controls.Presenters get { throw new NotSupportedException(); } } - /// - public override void Arranging(Size finalSize) - { - // We don't need to do anything here. - } - /// public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 7750ed7f4a..78ac75e428 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -84,7 +84,7 @@ namespace Avalonia.Controls.Presenters } /// - public override void Arranging(Size finalSize) + public override void UpdateControls() { CreateAndRemoveContainers(); ((ILogicalScrollable)Owner).InvalidateScroll(); diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 817d734506..b442c5bc61 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -78,14 +78,6 @@ namespace Avalonia.Controls.Presenters return _virtualizer?.BringIntoView(target, targetRect) ?? false; } - /// - protected override Size ArrangeOverride(Size finalSize) - { - var result = base.ArrangeOverride(finalSize); - _virtualizer.Arranging(finalSize); - return result; - } - /// protected override void PanelCreated(IPanel panel) { diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 2915ff07d5..77aae2548d 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Specialized; using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls { public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel { + private Size _availableSpace; private double _takenSpace; private int _canBeRemoved; private double _averageItemSize; @@ -20,15 +22,14 @@ namespace Avalonia.Controls get { return Orientation == Orientation.Horizontal ? - _takenSpace >= AvailableSpace.Width : - _takenSpace >= AvailableSpace.Height; + _takenSpace >= _availableSpace.Width : + _takenSpace >= _availableSpace.Height; } } + IVirtualizingController IVirtualizingPanel.Controller { get; set; } int IVirtualizingPanel.OverflowCount => _canBeRemoved; - Orientation IVirtualizingPanel.ScrollDirection => Orientation; - double IVirtualizingPanel.AverageItemSize => _averageItemSize; double IVirtualizingPanel.PixelOverflow @@ -55,16 +56,30 @@ namespace Avalonia.Controls } } - // TODO: We need to put a reasonable limit on this, probably based on the max window size. - private Size AvailableSpace => ((ILayoutable)this).PreviousMeasure ?? Bounds.Size; + private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller; + + protected override Size MeasureOverride(Size availableSize) + { + if (availableSize != ((ILayoutable)this).PreviousMeasure) + { + // TODO: We need to put a reasonable limit on this, probably based on the max + // window size. + _availableSpace = availableSize; + Controller?.UpdateControls(); + } + + return base.MeasureOverride(availableSize); + } protected override Size ArrangeOverride(Size finalSize) { + _availableSpace = finalSize; _canBeRemoved = 0; _takenSpace = 0; _averageItemSize = 0; _averageCount = 0; var result = base.ArrangeOverride(finalSize); + Controller?.UpdateControls(); return result; } @@ -103,7 +118,7 @@ namespace Avalonia.Controls rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height); child.Arrange(rect); - if (rect.Y >= AvailableSpace.Height) + if (rect.Y >= _availableSpace.Height) { ++_canBeRemoved; } @@ -120,7 +135,7 @@ namespace Avalonia.Controls rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height); child.Arrange(rect); - if (rect.X >= AvailableSpace.Width) + if (rect.X >= _availableSpace.Width) { ++_canBeRemoved; } @@ -139,7 +154,7 @@ namespace Avalonia.Controls var bounds = Bounds; var gap = Gap; - child.Measure(AvailableSpace); + child.Measure(_availableSpace); ++_averageCount; if (Orientation == Orientation.Vertical) @@ -173,6 +188,11 @@ namespace Avalonia.Controls _takenSpace -= width + gap; RemoveFromAverageItemSize(width); } + + if (_canBeRemoved > 0) + { + --_canBeRemoved; + } } private void AddToAverageItemSize(double value) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index d1443fb2ae..db2bb0f886 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls.UnitTests // and re-arrange everything. foreach (IControl i in target.GetSelfAndVisualDescendents()) { - i.InvalidateArrange(); + i.InvalidateMeasure(); } target.Arrange(new Rect(0, 0, 100, 100)); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 72ec2e6618..ef3297b5c3 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -65,9 +65,11 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = CreateTarget(); target.ApplyTemplate(); + target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + Assert.Equal(10, target.Panel.Children.Count); + target.Arrange(new Rect(0, 0, 100, 100)); Assert.Equal(10, target.Panel.Children.Count); } @@ -83,6 +85,20 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(2, target.Panel.Children.Count); } + [Fact] + public void Should_Expand_To_Fit_Containers_When_Flexible_Size() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(10, 200), target.DesiredSize); + Assert.Equal(new Size(10, 200), target.Bounds.Size); + Assert.Equal(20, target.Panel.Children.Count); + } + [Fact] public void Initial_Item_DataContexts_Should_Be_Correct() { diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 900f098198..d5217e007b 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -1,6 +1,7 @@ // 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 Moq; using Xunit; namespace Avalonia.Controls.UnitTests @@ -9,25 +10,52 @@ namespace Avalonia.Controls.UnitTests { public class Vertical { + [Fact] + public void Measure_Invokes_Controller_UpdateControls() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var controller = new Mock(); + + target.Controller = controller.Object; + target.Measure(new Size(100, 100)); + + controller.Verify(x => x.UpdateControls()); + } + + [Fact] + public void Arrange_Invokes_Controller_UpdateControls() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var controller = new Mock(); + + target.Controller = controller.Object; + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 110, 110)); + + controller.Verify(x => x.UpdateControls()); + } + [Fact] public void Reports_IsFull_False_Until_Measure_Height_Is_Reached() { var target = (IVirtualizingPanel)new VirtualizingStackPanel(); target.Measure(new Size(100, 100)); - target.Arrange(new Rect(target.DesiredSize)); Assert.Equal(new Size(0, 0), target.Bounds.Size); Assert.False(target.IsFull); + Assert.Equal(0, target.OverflowCount); target.Children.Add(new Canvas { Width = 50, Height = 50 }); Assert.False(target.IsFull); + Assert.Equal(0, target.OverflowCount); target.Children.Add(new Canvas { Width = 50, Height = 50 }); Assert.True(target.IsFull); + Assert.Equal(0, target.OverflowCount); } [Fact] - public void Reports_Overflow_Only_After_Arrange() + public void Reports_Overflow_After_Arrange() { var target = (IVirtualizingPanel)new VirtualizingStackPanel(); From acdf599dec7c7932815fa20c58e8f904cbe5f3fb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Jun 2016 20:40:23 +0200 Subject: [PATCH 019/122] Initial impl. of scrolling with arrow keys. - Currently only vertical implemented - Doesn't handle partially visible items at end of list --- Avalonia.sln | 4 ++ src/Avalonia.Controls/IVirtualizingPanel.cs | 13 ++++-- .../Presenters/ItemVirtualizer.cs | 17 +++++++ .../Presenters/ItemVirtualizerSimple.cs | 46 +++++++++++++++++-- .../Presenters/ItemsPresenter.cs | 9 +++- .../Presenters/ScrollContentPresenter.cs | 5 +- .../Primitives/ILogicalScrollable.cs | 12 ++++- src/Avalonia.Controls/StackPanel.cs | 12 +++++ .../VirtualizingStackPanel.cs | 18 ++++++++ ...ScrollContentPresenterTests_IScrollable.cs | 8 +++- .../VirtualizingStackPanelTests.cs | 20 ++++++++ 11 files changed, 151 insertions(+), 13 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index 928ba71ec8..9740e31ce2 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -161,6 +161,8 @@ EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4 + src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4 + src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{db070a10-bf39-4752-8456-86e9d5928478}*SharedItemsImports = 4 @@ -168,11 +170,13 @@ Global src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{78345174-5b52-4a14-b9fd-d5f2428137f0}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{54f237d5-a70a-4752-9656-0c70b1a7b047}*SharedItemsImports = 4 + samples\TestApplicationShared\TestApplicationShared.projitems*{ff69b927-c545-49ae-8e16-3d14d621aa12}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{811a76cf-1cf6-440f-963b-bbe31bd72a82}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 + samples\TestApplicationShared\TestApplicationShared.projitems*{8c923867-8a8f-4f6b-8b80-47d9e8436166}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{e3a1060b-50d0-44e8-88b6-f44ef2e5bd72}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index ca75517240..5d35fa1ec8 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -1,9 +1,6 @@ // 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.Controls.Primitives; - namespace Avalonia.Controls { /// @@ -25,11 +22,21 @@ namespace Avalonia.Controls /// /// Gets a value indicating whether the panel is full. /// + /// + /// This property should return false until enough children are added to fill the space + /// passed into the last measure in the direction of scroll. It should be updated + /// immediately after a child is added or removed. + /// bool IsFull { get; } /// /// Gets the number of items that can be removed while keeping the panel full. /// + /// + /// This property should return the number of children that are completely out of the + /// panel's current bounds in the direction of scroll. It should be updated after an + /// arrange. + /// int OverflowCount { get; } /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 8f77cff32b..d690d4768b 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; +using Avalonia.Input; using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters @@ -145,6 +146,17 @@ namespace Avalonia.Controls.Presenters { } + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + public virtual IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + return null; + } + /// /// Called when the items for the presenter change, either because /// has been set, the items collection has been @@ -168,5 +180,10 @@ namespace Avalonia.Controls.Presenters { return false; } + + /// + /// Invalidates the current scroll. + /// + protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll(); } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 78ac75e428..29094789a4 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -5,8 +5,8 @@ using System; using System.Collections; using System.Collections.Specialized; using System.Linq; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; +using Avalonia.Input; namespace Avalonia.Controls.Presenters { @@ -87,7 +87,7 @@ namespace Avalonia.Controls.Presenters public override void UpdateControls() { CreateAndRemoveContainers(); - ((ILogicalScrollable)Owner).InvalidateScroll(); + InvalidateScroll(); } /// @@ -134,7 +134,47 @@ namespace Avalonia.Controls.Presenters VirtualizingPanel.Children.Clear(); } - ((ILogicalScrollable)Owner).InvalidateScroll(); + InvalidateScroll(); + } + + public override IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + var generator = Owner.ItemContainerGenerator; + var itemIndex = generator.IndexFromContainer(from); + + if (itemIndex == -1) + { + return null; + } + + var newItemIndex = -1; + + if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) + { + switch (direction) + { + case FocusNavigationDirection.Up: + newItemIndex = itemIndex - 1; + break; + case FocusNavigationDirection.Down: + newItemIndex = itemIndex + 1; + break; + } + } + + if (newItemIndex >= 0 && newItemIndex < ItemCount) + { + + if (newItemIndex < FirstIndex || newItemIndex >= NextIndex) + { + OffsetValue += newItemIndex - itemIndex; + InvalidateScroll(); + } + + return generator.ContainerFromIndex(newItemIndex); + } + + return null; } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index b442c5bc61..21580a7cf2 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Specialized; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.VisualTree; using static Avalonia.Utilities.MathUtilities; namespace Avalonia.Controls.Presenters @@ -73,11 +72,17 @@ namespace Avalonia.Controls.Presenters Size ILogicalScrollable.PageScrollSize => new Size(0, 1); /// - bool ILogicalScrollable.BringIntoView(IVisual target, Rect targetRect) + bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect) { return _virtualizer?.BringIntoView(target, targetRect) ?? false; } + /// + IControl ILogicalScrollable.GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + return _virtualizer?.GetControlInDirection(direction, from); + } + /// protected override void PanelCreated(IPanel panel) { diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 0aeb61a529..c7a302f71d 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -118,10 +118,11 @@ namespace Avalonia.Controls.Presenters } var scrollable = Child as ILogicalScrollable; + var control = target as IControl; - if (scrollable?.IsLogicalScrollEnabled == true) + if (scrollable?.IsLogicalScrollEnabled == true && control != null) { - return scrollable.BringIntoView(target, targetRect); + return scrollable.BringIntoView(control, targetRect); } var transform = target.TransformToVisual(Child); diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs index b8b90f83a9..3fb201affc 100644 --- a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs +++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.VisualTree; +using Avalonia.Input; namespace Avalonia.Controls.Primitives { @@ -56,6 +56,14 @@ namespace Avalonia.Controls.Primitives /// The target visual. /// The portion of the target visual to bring into view. /// True if the scroll offset was changed; otherwise false. - bool BringIntoView(IVisual target, Rect targetRect); + bool BringIntoView(IControl target, Rect targetRect); + + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + IControl GetControlInDirection(FocusNavigationDirection direction, IControl from); } } diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 0284d84df3..c52b738d8b 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -73,6 +73,18 @@ namespace Avalonia.Controls /// The control from which movement begins. /// The control. IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) + { + var fromControl = from as IControl; + return (fromControl != null) ? GetControlInDirection(direction, fromControl) : null; + } + + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + protected virtual IInputElement GetControlInDirection(FocusNavigationDirection direction, IControl from) { var horiz = Orientation == Orientation.Horizontal; int index = Children.IndexOf((IControl)from); diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 77aae2548d..e4f639bdcb 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Specialized; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.VisualTree; @@ -107,6 +110,21 @@ namespace Avalonia.Controls } } + protected override IInputElement GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + var logicalScrollable = Parent as ILogicalScrollable; + var fromControl = from as IControl; + + if (logicalScrollable?.IsLogicalScrollEnabled == true && fromControl != null) + { + return logicalScrollable.GetControlInDirection(direction, fromControl); + } + else + { + return base.GetControlInDirection(direction, from); + } + } + internal override void ArrangeChild( IControl child, Rect rect, diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs index d0c6a386ac..142df9c6eb 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs @@ -5,6 +5,7 @@ using System; using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.VisualTree; using Xunit; @@ -293,7 +294,7 @@ namespace Avalonia.Controls.UnitTests } } - public bool BringIntoView(IVisual target, Rect targetRect) + public bool BringIntoView(IControl target, Rect targetRect) { throw new NotImplementedException(); } @@ -303,6 +304,11 @@ namespace Avalonia.Controls.UnitTests AvailableSize = availableSize; return new Size(150, 150); } + + public IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + throw new NotImplementedException(); + } } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index d5217e007b..9034076444 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -1,6 +1,9 @@ // 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.Primitives; +using Avalonia.Input; +using Avalonia.LogicalTree; using Moq; using Xunit; @@ -42,6 +45,7 @@ namespace Avalonia.Controls.UnitTests target.Measure(new Size(100, 100)); + Assert.Equal(new Size(0, 0), target.DesiredSize); Assert.Equal(new Size(0, 0), target.Bounds.Size); Assert.False(target.IsFull); @@ -75,6 +79,22 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, target.OverflowCount); } + + [Fact] + public void Passes_Navigation_Request_To_ILogicalScrollable_Parent() + { + var presenter = new Mock().As(); + var scrollable = presenter.As(); + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var from = new Canvas(); + + scrollable.Setup(x => x.IsLogicalScrollEnabled).Returns(true); + + ((ISetLogicalParent)target).SetParent(presenter.Object); + ((INavigableContainer)target).GetControl(FocusNavigationDirection.Next, from); + + scrollable.Verify(x => x.GetControlInDirection(FocusNavigationDirection.Next, from)); + } } } } \ No newline at end of file From 7a718ef9c29f19ab0aa39b59a075c6e913c06bb4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Jun 2016 23:54:17 +0200 Subject: [PATCH 020/122] Updated overflow algorithm. --- .../VirtualizingStackPanel.cs | 3 +- .../VirtualizingStackPanelTests.cs | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index e4f639bdcb..9b2e1ffe21 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -41,7 +41,7 @@ namespace Avalonia.Controls { var bounds = Orientation == Orientation.Horizontal ? Bounds.Width : Bounds.Height; - return Math.Max(0, (_takenSpace - _pixelOffset) - bounds); + return Math.Max(0, _takenSpace - bounds); } } @@ -82,6 +82,7 @@ namespace Avalonia.Controls _averageItemSize = 0; _averageCount = 0; var result = base.ArrangeOverride(finalSize); + _takenSpace += _pixelOffset; Controller?.UpdateControls(); return result; } diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 9034076444..a024fb5ec5 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -80,6 +80,52 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, target.OverflowCount); } + [Fact] + public void Reports_PixelOverflow_After_Arrange() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + target.Children.Add(new Canvas { Width = 50, Height = 52 }); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(2, target.PixelOverflow); + } + + [Fact] + public void Reports_PixelOverflow_With_PixelOffset() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + target.Children.Add(new Canvas { Width = 50, Height = 52 }); + target.PixelOffset = 2; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(2, target.PixelOverflow); + } + + [Fact] + public void PixelOffset_Can_Be_More_Than_Child_Without_Affecting_IsFull() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + target.Children.Add(new Canvas { Width = 50, Height = 52 }); + target.PixelOffset = 55; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(55, target.PixelOffset); + Assert.Equal(2, target.PixelOverflow); + Assert.True(target.IsFull); + } + [Fact] public void Passes_Navigation_Request_To_ILogicalScrollable_Parent() { From 81b66596c01da02d855ad77796ac4cacf4681c96 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Jun 2016 23:58:44 +0200 Subject: [PATCH 021/122] Use a full item's offset for overflow. --- .../Presenters/ItemVirtualizerSimple.cs | 9 ++++++--- .../ItemsPresenterTests_Virtualization_Simple.cs | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 29094789a4..e165389c79 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -48,7 +48,9 @@ namespace Avalonia.Controls.Presenters if (delta != 0) { - if ((NextIndex - 1) + delta < ItemCount) + var newLastIndex = (NextIndex - 1) + delta; + + if (newLastIndex < ItemCount) { if (panel.PixelOffset > 0) { @@ -63,10 +65,11 @@ namespace Avalonia.Controls.Presenters } else { - // We're moving to a partially obscured item at the end of the list. + // We're moving to a partially obscured item at the end of the list so + // offset the panel by the height of the first item. var firstIndex = ItemCount - panel.Children.Count; RecycleContainersForMove(firstIndex - FirstIndex); - panel.PixelOffset = VirtualizingPanel.PixelOverflow; + panel.PixelOffset = panel.Children[0].Bounds.Height; } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 1c7a08bdb4..085d420d16 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -142,7 +142,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); Assert.Equal(10, minIndex); - Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); + Assert.Equal(10, ((IVirtualizingPanel)target.Panel).PixelOffset); ((ILogicalScrollable)target).Offset = new Vector(0, 10); @@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests.Presenters minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); Assert.Equal(10, minIndex); - Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); + Assert.Equal(10, ((IVirtualizingPanel)target.Panel).PixelOffset); } [Fact] From cba5200645d1e8b669608f8512b1d846b13c457f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jun 2016 00:29:13 +0200 Subject: [PATCH 022/122] Fix keyboard nav with partially visible items. --- .../Presenters/ItemVirtualizerSimple.cs | 7 +- ...emsPresenterTests_Virtualization_Simple.cs | 96 +++++++++++++++++-- 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index e165389c79..788ed10379 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -143,6 +143,7 @@ namespace Avalonia.Controls.Presenters public override IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) { var generator = Owner.ItemContainerGenerator; + var panel = VirtualizingPanel; var itemIndex = generator.IndexFromContainer(from); if (itemIndex == -1) @@ -167,8 +168,12 @@ namespace Avalonia.Controls.Presenters if (newItemIndex >= 0 && newItemIndex < ItemCount) { + // Get the index of the first and last fully visible items (i.e. excluding any + // partially visible item at the beginning or end). + var firstIndex = panel.PixelOffset == 0 ? FirstIndex : FirstIndex + 1; + var lastIndex = (FirstIndex + ViewportValue) - 1; - if (newItemIndex < FirstIndex || newItemIndex >= NextIndex) + if (newItemIndex < firstIndex || newItemIndex > lastIndex) { OffsetValue += newItemIndex - itemIndex; InvalidateScroll(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 085d420d16..8c1b0cfa57 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Input; using Xunit; namespace Avalonia.Controls.UnitTests.Presenters @@ -131,7 +132,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 95)); @@ -162,7 +163,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Inserting_Items_Should_Update_Containers() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -187,7 +188,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Removing_First_Materialized_Item_Should_Update_Containers() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -209,7 +210,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Removing_Items_From_Middle_Should_Update_Containers_When_All_Items_Visible() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 200)); @@ -234,7 +235,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Removing_Last_Item_Should_Update_Containers_When_All_Items_Visible() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 200)); @@ -258,7 +259,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Removing_Items_When_Scrolled_To_End_Should_Recyle_Containers_At_Top() { - var target = CreateTarget(itemCount: 20, useAvaloniaList: true); + var target = CreateTarget(useAvaloniaList: true); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -282,7 +283,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Removing_Items_When_Scrolled_To_Near_End_Should_Recycle_Containers_At_Bottom_And_Top() { - var target = CreateTarget(itemCount: 20, useAvaloniaList: true); + var target = CreateTarget(useAvaloniaList: true); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -308,7 +309,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Replacing_Items_Should_Update_Containers() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -329,7 +330,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Moving_Items_Should_Update_Containers() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -353,7 +354,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Setting_Items_To_Null_Should_Remove_Containers() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -370,6 +371,81 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Empty(target.Panel.Children); } + public class Vertical + { + [Fact] + public void GetControlInDirection_Down_Should_Return_Existing_Container_If_Materialized() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var from = target.Panel.Children[5]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Down, + from); + + Assert.Same(target.Panel.Children[6], result); + } + + [Fact] + public void GetControlInDirection_Down_Should_Scroll_If_Necessary() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var from = target.Panel.Children[9]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Down, + from); + + Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[9], result); + } + + [Fact] + public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 95)); + target.Arrange(new Rect(0, 0, 100, 95)); + + var from = target.Panel.Children[8]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Down, + from); + + Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[8], result); + } + + [Fact] + public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Is_Currently_Shown() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 95)); + target.Arrange(new Rect(0, 0, 100, 95)); + ((ILogicalScrollable)target).Offset = new Vector(0, 11); + + var from = target.Panel.Children[1]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Up, + from); + + Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[0], result); + } + } + public class WithContainers { [Fact] From e8dd81fa201aa8030b79037c0196af6c1b979b87 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jun 2016 21:50:43 +0200 Subject: [PATCH 023/122] Support horiz keyboard movement. --- .../Presenters/ItemVirtualizerSimple.cs | 12 +++ ...emsPresenterTests_Virtualization_Simple.cs | 77 ++++++++++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 788ed10379..a6ca5dc18c 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -165,6 +165,18 @@ namespace Avalonia.Controls.Presenters break; } } + else + { + switch (direction) + { + case FocusNavigationDirection.Left: + newItemIndex = itemIndex - 1; + break; + case FocusNavigationDirection.Right: + newItemIndex = itemIndex + 1; + break; + } + } if (newItemIndex >= 0 && newItemIndex < ItemCount) { diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 8c1b0cfa57..f70915ad3c 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -427,7 +427,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Is_Currently_Shown() + public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() { var target = CreateTarget(); @@ -446,6 +446,81 @@ namespace Avalonia.Controls.UnitTests.Presenters } } + public class Horizontal + { + [Fact] + public void GetControlInDirection_Right_Should_Return_Existing_Container_If_Materialized() + { + var target = CreateTarget(orientation: Orientation.Horizontal); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var from = target.Panel.Children[5]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Right, + from); + + Assert.Same(target.Panel.Children[6], result); + } + + [Fact] + public void GetControlInDirection_Right_Should_Scroll_If_Necessary() + { + var target = CreateTarget(orientation: Orientation.Horizontal); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var from = target.Panel.Children[9]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Right, + from); + + Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[9], result); + } + + [Fact] + public void GetControlInDirection_Right_Should_Scroll_If_Partially_Visible() + { + var target = CreateTarget(orientation: Orientation.Horizontal); + + target.ApplyTemplate(); + target.Measure(new Size(95, 100)); + target.Arrange(new Rect(0, 0, 95, 100)); + + var from = target.Panel.Children[8]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Right, + from); + + Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[8], result); + } + + [Fact] + public void GetControlInDirection_Left_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() + { + var target = CreateTarget(orientation: Orientation.Horizontal); + + target.ApplyTemplate(); + target.Measure(new Size(95, 100)); + target.Arrange(new Rect(0, 0, 95, 100)); + ((ILogicalScrollable)target).Offset = new Vector(11, 0); + + var from = target.Panel.Children[1]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Left, + from); + + Assert.Equal(new Vector(10, 0), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[0], result); + } + } + public class WithContainers { [Fact] From 635ee4fdb2b3566924843367372284da3b390143 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jun 2016 22:19:48 +0200 Subject: [PATCH 024/122] Fix compile error. --- samples/XamlTestApplicationPcl/TestScrollable.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs index 39e708d043..c2bf2f005a 100644 --- a/samples/XamlTestApplicationPcl/TestScrollable.cs +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -2,6 +2,7 @@ using System; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Media; using Avalonia.VisualTree; @@ -75,7 +76,12 @@ namespace XamlTestApplication } } - public bool BringIntoView(IVisual target, Rect targetRect) + public bool BringIntoView(IControl target, Rect targetRect) + { + throw new NotImplementedException(); + } + + public IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) { throw new NotImplementedException(); } From ae2ea31c6e6f6ef14539fe7b089e54078cec15fb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jun 2016 23:44:21 +0200 Subject: [PATCH 025/122] Allow changing virtualization mode in test app. Not yet working correctly. --- samples/VirtualizationTest/MainWindow.xaml | 5 ++- .../ViewModels/MainWindowViewModel.cs | 12 +++++++ .../Presenters/ItemVirtualizer.cs | 12 ++++++- .../Presenters/ItemsPresenter.cs | 18 ++++++++++ .../ItemsPresenterTests_Virtualization.cs | 35 +++++++++++++++++++ 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index 94a0fc1b68..681de0a815 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -5,6 +5,8 @@ Margin="16 0 0 0" MinWidth="150" Gap="4"> + @@ -28,7 +30,8 @@ + SelectionMode="Multiple" + VirtualizationMode="{Binding VirtualizationMode}"> diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 0e1f56fa07..3e07aa54e9 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Linq; using Avalonia.Collections; +using Avalonia.Controls; using ReactiveUI; namespace VirtualizationTest.ViewModels @@ -15,6 +17,7 @@ namespace VirtualizationTest.ViewModels private int _newItemIndex; private IReactiveList _items; private string _prefix = "Item"; + private ItemVirtualizationMode _virtualizationMode = ItemVirtualizationMode.Simple; public MainWindowViewModel() { @@ -50,6 +53,15 @@ namespace VirtualizationTest.ViewModels private set { this.RaiseAndSetIfChanged(ref _items, value); } } + public ItemVirtualizationMode VirtualizationMode + { + get { return _virtualizationMode; } + set { this.RaiseAndSetIfChanged(ref _virtualizationMode, value); } + } + + public IEnumerable VirtualizationModes => + Enum.GetValues(typeof(ItemVirtualizationMode)).Cast(); + public ReactiveCommand AddItemCommand { get; private set; } public ReactiveCommand RecreateCommand { get; private set; } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index d690d4768b..c32e225bb0 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -14,8 +14,10 @@ namespace Avalonia.Controls.Presenters /// /// Base class for classes which handle virtualization for an . /// - internal abstract class ItemVirtualizer : IVirtualizingController + internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable { + private bool disposedValue; + /// /// Initializes a new instance of the class. /// @@ -181,6 +183,14 @@ namespace Avalonia.Controls.Presenters return false; } + /// + public virtual void Dispose() + { + VirtualizingPanel.Controller = null; + VirtualizingPanel.Children.Clear(); + Owner.ItemContainerGenerator.Clear(); + } + /// /// Invalidates the current scroll. /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 21580a7cf2..d06d083220 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -32,6 +32,9 @@ namespace Avalonia.Controls.Presenters KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( typeof(ItemsPresenter), KeyboardNavigationMode.Once); + + VirtualizationModeProperty.Changed + .AddClassHandler(x => x.VirtualizationModeChanged); } /// @@ -113,5 +116,20 @@ namespace Avalonia.Controls.Presenters var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0); return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY)); } + + private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e) + { + _virtualizer?.Dispose(); + _virtualizer = ItemVirtualizer.Create(this); + + if (Items != null && Panel != null) + { + _virtualizer.ItemsChanged( + Items, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); + } } } \ 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 ef3297b5c3..5351231fec 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -59,6 +59,20 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.True(((ILogicalScrollable)target).IsLogicalScrollEnabled); } + [Fact] + public void Parent_ScrollContentPresenter_Properties_Should_Be_Set() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var scroll = (ScrollContentPresenter)target.Parent; + Assert.Equal(new Size(0, 20), scroll.Extent); + Assert.Equal(new Size(0, 10), scroll.Viewport); + } + [Fact] public void Should_Fill_Panel_With_Containers() { @@ -138,6 +152,27 @@ namespace Avalonia.Controls.UnitTests.Presenters } } + [Fact] + public void Changing_VirtualizationMode_Simple_To_None_Should_Update_Scroll_Properties() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var scroll = (ScrollContentPresenter)target.Parent; + Assert.Equal(10, target.Panel.Children.Count); + Assert.Equal(new Size(0, 20), scroll.Extent); + Assert.Equal(new Size(0, 10), scroll.Viewport); + + target.VirtualizationMode = ItemVirtualizationMode.None; + + Assert.Equal(20, target.Panel.Children.Count); + Assert.Equal(new Size(0, 200), scroll.Extent); + Assert.Equal(new Size(0, 100), scroll.Viewport); + } + private static ItemsPresenter CreateTarget( ItemVirtualizationMode mode = ItemVirtualizationMode.Simple, Orientation orientation = Orientation.Vertical, From 7e4f667d8907cd0cace397b9812a3b1ca908bf6b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jun 2016 23:51:14 +0200 Subject: [PATCH 026/122] Fix PixelOverflow during arrange. --- .../VirtualizingStackPanel.cs | 2 +- .../VirtualizingStackPanelTests.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 9b2e1ffe21..0e40011760 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -40,7 +40,7 @@ namespace Avalonia.Controls get { var bounds = Orientation == Orientation.Horizontal ? - Bounds.Width : Bounds.Height; + _availableSpace.Width : _availableSpace.Height; return Math.Max(0, _takenSpace - bounds); } } diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index a024fb5ec5..8ed7f9d0bb 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -80,6 +80,30 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, target.OverflowCount); } + [Fact] + public void Reports_Correct_Overflow_During_Arrange() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var controller = new Mock(); + var called = false; + + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + target.Children.Add(new Canvas { Width = 50, Height = 52 }); + target.Measure(new Size(100, 100)); + + controller.Setup(x => x.UpdateControls()).Callback(() => + { + Assert.Equal(2, target.PixelOverflow); + Assert.Equal(0, target.OverflowCount); + called = true; + }); + + target.Controller = controller.Object; + target.Arrange(new Rect(target.DesiredSize)); + + Assert.True(called); + } + [Fact] public void Reports_PixelOverflow_After_Arrange() { From 32760bbc088f6119f63ff8865665800434b35d9c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 00:09:46 +0200 Subject: [PATCH 027/122] Fix changing virtualization mode in theory. That is; tests pass. In practise in the virtualization test app it still doesn't quite work. --- .../Presenters/ItemVirtualizer.cs | 2 ++ .../Presenters/ItemVirtualizerNone.cs | 4 +++ .../Presenters/ItemVirtualizerSimple.cs | 4 +++ .../Presenters/ItemsPresenter.cs | 8 ----- .../Presenters/ScrollContentPresenter.cs | 3 +- .../ItemsPresenterTests_Virtualization.cs | 31 +++++++++++++++++-- 6 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index c32e225bb0..8bc7bd9121 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -25,6 +25,8 @@ namespace Avalonia.Controls.Presenters public ItemVirtualizer(ItemsPresenter owner) { Owner = owner; + Items = owner.Items; + ItemCount = owner.Items.Count(); } /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 7450fdb2b4..959afe0065 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -19,6 +19,10 @@ namespace Avalonia.Controls.Presenters public ItemVirtualizerNone(ItemsPresenter owner) : base(owner) { + if (Items != null && owner.Panel != null) + { + AddContainers(0, Items); + } } /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index a6ca5dc18c..f67a46308e 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -23,6 +23,10 @@ namespace Avalonia.Controls.Presenters public ItemVirtualizerSimple(ItemsPresenter owner) : base(owner) { + if (Items != null && VirtualizingPanel != null) + { + CreateAndRemoveContainers(); + } } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index d06d083220..7298535b15 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -121,14 +121,6 @@ namespace Avalonia.Controls.Presenters { _virtualizer?.Dispose(); _virtualizer = ItemVirtualizer.Create(this); - - if (Items != null && Panel != null) - { - _virtualizer.ItemsChanged( - Items, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); } } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index c7a302f71d..5e20e8d3c9 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -290,8 +290,9 @@ namespace Avalonia.Controls.Presenters if (!scrollable.IsLogicalScrollEnabled) { Offset = default(Vector); - InvalidateMeasure(); } + + InvalidateMeasure(); } if (scrollable.IsLogicalScrollEnabled) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 5351231fec..d8ebbe50c6 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -153,7 +153,30 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Changing_VirtualizationMode_Simple_To_None_Should_Update_Scroll_Properties() + public void Changing_VirtualizationMode_None_To_Simple_Should_Update_Control() + { + var target = CreateTarget(mode: ItemVirtualizationMode.None); + var scroll = (ScrollContentPresenter)target.Parent; + + target.ApplyTemplate(); + scroll.Measure(new Size(100, 100)); + 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, 100), scroll.Viewport); + + target.VirtualizationMode = ItemVirtualizationMode.Simple; + scroll.Measure(new Size(100, 100)); + scroll.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(10, target.Panel.Children.Count); + Assert.Equal(new Size(0, 20), scroll.Extent); + Assert.Equal(new Size(0, 10), scroll.Viewport); + } + + [Fact] + public void Changing_VirtualizationMode_Simple_To_None_Should_Update_Control() { var target = CreateTarget(); @@ -167,10 +190,12 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(new Size(0, 10), scroll.Viewport); target.VirtualizationMode = ItemVirtualizationMode.None; + scroll.Measure(new Size(100, 100)); + scroll.Arrange(new Rect(0, 0, 100, 100)); Assert.Equal(20, target.Panel.Children.Count); - Assert.Equal(new Size(0, 200), scroll.Extent); - Assert.Equal(new Size(0, 100), scroll.Viewport); + Assert.Equal(new Size(10, 200), scroll.Extent); + Assert.Equal(new Size(100, 100), scroll.Viewport); } private static ItemsPresenter CreateTarget( From 5467841e3e46e830e555fc1842af667a3e7c658b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 01:08:53 +0200 Subject: [PATCH 028/122] Fixed unit test name. --- ...e.cs => ScrollContentPresenterTests_ILogicalScrollable.cs} | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) rename tests/Avalonia.Controls.UnitTests/Presenters/{ScrollContentPresenterTests_IScrollable.cs => ScrollContentPresenterTests_ILogicalScrollable.cs} (98%) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs similarity index 98% rename from tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs rename to tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs index 142df9c6eb..8f3bee3bb9 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs @@ -6,13 +6,11 @@ using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.Layout; -using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests { - public class ScrollContentPresenterTests_IScrollable + public class ScrollContentPresenterTests_ILogicalScrollable { [Fact] public void Measure_Should_Pass_Unchanged_Bounds_To_IScrollable() From 4c000b9ce4b1fcc12dc2446b7d72ee60e7b2b340 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 01:33:20 +0200 Subject: [PATCH 029/122] Fixed ScrollContentPresenter. Did not react correctly to child changing. --- .../Presenters/ScrollContentPresenter.cs | 18 ++++--- .../Avalonia.Controls.UnitTests.csproj | 2 +- ...ontentPresenterTests_ILogicalScrollable.cs | 50 +++++++++++++++++++ 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 5e20e8d3c9..25fe728d49 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -59,6 +59,7 @@ namespace Avalonia.Controls.Presenters static ScrollContentPresenter() { ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); + ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); AffectsArrange(OffsetProperty); } @@ -258,6 +259,16 @@ namespace Avalonia.Controls.Presenters e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect); } + private void ChildChanged(AvaloniaPropertyChangedEventArgs e) + { + UpdateScrollableSubscription((IControl)e.NewValue); + + if (e.OldValue != null) + { + Offset = default(Vector); + } + } + private void UpdateScrollableSubscription(IControl child) { var scrollable = child as ILogicalScrollable; @@ -286,12 +297,7 @@ namespace Avalonia.Controls.Presenters if (logicalScroll != scrollable.IsLogicalScrollEnabled) { UpdateScrollableSubscription(Child); - - if (!scrollable.IsLogicalScrollEnabled) - { - Offset = default(Vector); - } - + Offset = default(Vector); InvalidateMeasure(); } diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index d3a48c02b0..3732f13769 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -109,7 +109,7 @@ - + diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs index 8f3bee3bb9..5a7e5f9f8b 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs @@ -236,6 +236,56 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); } + [Fact] + public void Changing_Content_Should_Update_State() + { + var logicalScrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + Viewport = new Size(25, 25), + }; + + var nonLogicalScrollable = new TestScrollable + { + IsLogicalScrollEnabled = false, + }; + + var target = new ScrollContentPresenter + { + Content = logicalScrollable, + }; + + target.UpdateChild(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(logicalScrollable.Extent, target.Extent); + Assert.Equal(logicalScrollable.Offset, target.Offset); + Assert.Equal(logicalScrollable.Viewport, target.Viewport); + Assert.Equal(new Rect(0, 0, 100, 100), logicalScrollable.Bounds); + + target.Content = nonLogicalScrollable; + target.UpdateChild(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Size(150, 150), target.Extent); + Assert.Equal(new Vector(0, 0), target.Offset); + Assert.Equal(new Size(100, 100), target.Viewport); + Assert.Equal(new Rect(0, 0, 150, 150), nonLogicalScrollable.Bounds); + + target.Content = logicalScrollable; + target.UpdateChild(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(logicalScrollable.Extent, target.Extent); + Assert.Equal(logicalScrollable.Offset, target.Offset); + Assert.Equal(logicalScrollable.Viewport, target.Viewport); + Assert.Equal(new Rect(0, 0, 100, 100), logicalScrollable.Bounds); + } + private class TestScrollable : Control, ILogicalScrollable { private Size _extent; From 8e6976a0da9359f7f9a3d92edf68cf8451a8cfbc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 18:47:13 +0200 Subject: [PATCH 030/122] Tweak measure/arrange algorithm. To ensure that parent controls are measured/arranged before children. --- src/Avalonia.Layout/LayoutManager.cs | 35 +++++++++++++++++++++------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 42ed45b0bb..11119e9b4a 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Logging; using Avalonia.Threading; @@ -13,8 +14,8 @@ namespace Avalonia.Layout /// public class LayoutManager : ILayoutManager { - private readonly Queue _toMeasure = new Queue(); - private readonly Queue _toArrange = new Queue(); + private readonly HashSet _toMeasure = new HashSet(); + private readonly HashSet _toArrange = new HashSet(); private bool _queued; private bool _running; @@ -29,8 +30,8 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toMeasure.Enqueue(control); - _toArrange.Enqueue(control); + _toMeasure.Add(control); + _toArrange.Add(control); QueueLayoutPass(); } @@ -40,7 +41,7 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toArrange.Enqueue(control); + _toArrange.Add(control); QueueLayoutPass(); } @@ -107,7 +108,7 @@ namespace Avalonia.Layout { while (_toMeasure.Count > 0) { - var next = _toMeasure.Dequeue(); + var next = _toMeasure.First(); Measure(next); } } @@ -116,7 +117,7 @@ namespace Avalonia.Layout { while (_toArrange.Count > 0 && _toMeasure.Count == 0) { - var next = _toArrange.Dequeue(); + var next = _toArrange.First(); Arrange(next); } } @@ -124,29 +125,45 @@ namespace Avalonia.Layout private void Measure(ILayoutable control) { var root = control as ILayoutRoot; + var parent = control.VisualParent as ILayoutable; if (root != null) { root.Measure(root.MaxClientSize); } - else if (control.PreviousMeasure.HasValue) + else if (parent != null) + { + Measure(parent); + } + + if (!control.IsMeasureValid) { control.Measure(control.PreviousMeasure.Value); } + + _toMeasure.Remove(control); } private void Arrange(ILayoutable control) { var root = control as ILayoutRoot; + var parent = control.VisualParent as ILayoutable; if (root != null) { root.Arrange(new Rect(root.DesiredSize)); } - else if (control.PreviousArrange.HasValue) + else if (parent != null) + { + Measure(parent); + } + + if (control.PreviousArrange.HasValue) { control.Arrange(control.PreviousArrange.Value); } + + _toArrange.Remove(control); } private void QueueLayoutPass() From 67647f925fe68035385bb24faf325d57708078d0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 19:27:52 +0200 Subject: [PATCH 031/122] Tidied ScrollContentPresenter logic. --- src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 25fe728d49..e0061ed386 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -280,7 +280,7 @@ namespace Avalonia.Controls.Presenters { scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable); - if (scrollable?.IsLogicalScrollEnabled == true) + if (scrollable.IsLogicalScrollEnabled == true) { _logicalScrollSubscription = new CompositeDisposable( this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x), @@ -300,8 +300,7 @@ namespace Avalonia.Controls.Presenters Offset = default(Vector); InvalidateMeasure(); } - - if (scrollable.IsLogicalScrollEnabled) + else if (scrollable.IsLogicalScrollEnabled) { Viewport = scrollable.Viewport; Extent = scrollable.Extent; From 13642613cf39a752d1200599db0067fd82ca23af Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 19:37:52 +0200 Subject: [PATCH 032/122] Added failing test. --- .../ItemsPresenterTests_Virtualization.cs | 46 ++++++++++++++++--- tests/Avalonia.UnitTests/TestServices.cs | 3 ++ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index d8ebbe50c6..26657a0fbb 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -7,6 +7,8 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Layout; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests.Presenters @@ -158,7 +160,6 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = CreateTarget(mode: ItemVirtualizationMode.None); var scroll = (ScrollContentPresenter)target.Parent; - target.ApplyTemplate(); scroll.Measure(new Size(100, 100)); scroll.Arrange(new Rect(0, 0, 100, 100)); @@ -167,29 +168,60 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(new Size(100, 100), scroll.Viewport); target.VirtualizationMode = ItemVirtualizationMode.Simple; - scroll.Measure(new Size(100, 100)); - scroll.Arrange(new Rect(0, 0, 100, 100)); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); Assert.Equal(10, target.Panel.Children.Count); Assert.Equal(new Size(0, 20), scroll.Extent); Assert.Equal(new Size(0, 10), scroll.Viewport); } + [Fact] + public void Changing_VirtualizationMode_None_To_Simple_Should_Add_Correct_Number_Of_Controls() + { + using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + { + var target = CreateTarget(mode: ItemVirtualizationMode.None); + var scroll = (ScrollContentPresenter)target.Parent; + + scroll.Measure(new Size(100, 100)); + scroll.Arrange(new Rect(0, 0, 100, 100)); + + // Ensure than an intermediate measure pass doesn't add more controls than it + // should. This can happen if target gets measured with Size.Infinity which + // is what the available size should be when VirtualizationMode == None but not + // what it should after VirtualizationMode is changed to Simple. + target.Panel.Children.CollectionChanged += (s, e) => + { + Assert.InRange(target.Panel.Children.Count, 0, 10); + }; + + target.VirtualizationMode = ItemVirtualizationMode.Simple; + LayoutManager.Instance.ExecuteLayoutPass(); + + Assert.Equal(10, target.Panel.Children.Count); + } + } + [Fact] public void Changing_VirtualizationMode_Simple_To_None_Should_Update_Control() { var target = CreateTarget(); + var scroll = (ScrollContentPresenter)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + scroll.Measure(new Size(100, 100)); + scroll.Arrange(new Rect(0, 0, 100, 100)); - var scroll = (ScrollContentPresenter)target.Parent; Assert.Equal(10, target.Panel.Children.Count); Assert.Equal(new Size(0, 20), scroll.Extent); Assert.Equal(new Size(0, 10), scroll.Viewport); target.VirtualizationMode = ItemVirtualizationMode.None; + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + // Here - unlike changing the other way - we need to do a layout pass on the scroll + // content presenter as non-logical scroll values are only updated on arrange. scroll.Measure(new Size(100, 100)); scroll.Arrange(new Rect(0, 0, 100, 100)); diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 9ea13e07ff..c4a0c98908 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -44,6 +44,9 @@ namespace Avalonia.UnitTests keyboardDevice: () => new KeyboardDevice(), inputManager: new InputManager()); + public static readonly TestServices RealLayoutManager = new TestServices( + layoutManager: new LayoutManager()); + public static readonly TestServices RealStyler = new TestServices( styler: new Styler()); From 8ed3a7bc7cf0da47d632ac3df069f920a1ce6375 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 19:48:20 +0200 Subject: [PATCH 033/122] Fix copy/paste error in LayoutManager. Fixes test previously added. --- src/Avalonia.Layout/LayoutManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 11119e9b4a..85daff28b9 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -155,7 +155,7 @@ namespace Avalonia.Layout } else if (parent != null) { - Measure(parent); + Arrange(parent); } if (control.PreviousArrange.HasValue) From 37390e8b33fb7c3a293c733252db9ff0bcc391fa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 19:49:06 +0200 Subject: [PATCH 034/122] Don't create items immediately... ...in virtualized ItemsPresenters. --- src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index f67a46308e..d25dcdde61 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -23,10 +23,8 @@ namespace Avalonia.Controls.Presenters public ItemVirtualizerSimple(ItemsPresenter owner) : base(owner) { - if (Items != null && VirtualizingPanel != null) - { - CreateAndRemoveContainers(); - } + // Don't need to add children here as UpdateControls should be called by the panel + // measure/arrange. } /// From d0942f84a98f34c2e1de8c5b301df608426d0df5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 20:36:15 +0200 Subject: [PATCH 035/122] FocusNavigationDirection => NavigationDirection And added page up/down. --- .../XamlTestApplicationPcl/TestScrollable.cs | 2 +- src/Avalonia.Controls/Canvas.cs | 2 +- .../Presenters/ItemVirtualizer.cs | 2 +- .../Presenters/ItemVirtualizerSimple.cs | 10 ++-- .../Presenters/ItemsPresenter.cs | 2 +- .../Primitives/ILogicalScrollable.cs | 2 +- src/Avalonia.Controls/StackPanel.cs | 20 +++---- .../VirtualizingStackPanel.cs | 2 +- src/Avalonia.Controls/WrapPanel.cs | 18 +++--- src/Avalonia.Input/Avalonia.Input.csproj | 4 +- .../IKeyboardNavigationHandler.cs | 2 +- src/Avalonia.Input/INavigableContainer.cs | 2 +- .../KeyboardNavigationHandler.cs | 22 +++---- .../Navigation/DirectionalNavigation.cs | 26 ++++----- .../Navigation/TabNavigation.cs | 26 ++++----- ...ionDirection.cs => NavigationDirection.cs} | 14 ++++- ...emsPresenterTests_Virtualization_Simple.cs | 16 ++--- ...ontentPresenterTests_ILogicalScrollable.cs | 2 +- .../VirtualizingStackPanelTests.cs | 4 +- .../KeyboardNavigationTests_Arrows.cs | 44 +++++++------- .../KeyboardNavigationTests_Tab.cs | 58 +++++++++---------- 21 files changed, 145 insertions(+), 135 deletions(-) rename src/Avalonia.Input/{FocusNavigationDirection.cs => NavigationDirection.cs} (78%) diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs index c2bf2f005a..9d4c1d9b29 100644 --- a/samples/XamlTestApplicationPcl/TestScrollable.cs +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -81,7 +81,7 @@ namespace XamlTestApplication throw new NotImplementedException(); } - public IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + public IControl GetControlInDirection(NavigationDirection direction, IControl from) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Controls/Canvas.cs b/src/Avalonia.Controls/Canvas.cs index ebccb7435e..74eebea4bc 100644 --- a/src/Avalonia.Controls/Canvas.cs +++ b/src/Avalonia.Controls/Canvas.cs @@ -136,7 +136,7 @@ namespace Avalonia.Controls /// The movement direction. /// The control from which movement begins. /// The control. - IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) { // TODO: Implement this return null; diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 8bc7bd9121..4112c849d2 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -156,7 +156,7 @@ namespace Avalonia.Controls.Presenters /// The movement direction. /// The control from which movement begins. /// The control. - public virtual IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + public virtual IControl GetControlInDirection(NavigationDirection direction, IControl from) { return null; } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index d25dcdde61..e9fbd33c72 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -142,7 +142,7 @@ namespace Avalonia.Controls.Presenters InvalidateScroll(); } - public override IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + public override IControl GetControlInDirection(NavigationDirection direction, IControl from) { var generator = Owner.ItemContainerGenerator; var panel = VirtualizingPanel; @@ -159,10 +159,10 @@ namespace Avalonia.Controls.Presenters { switch (direction) { - case FocusNavigationDirection.Up: + case NavigationDirection.Up: newItemIndex = itemIndex - 1; break; - case FocusNavigationDirection.Down: + case NavigationDirection.Down: newItemIndex = itemIndex + 1; break; } @@ -171,10 +171,10 @@ namespace Avalonia.Controls.Presenters { switch (direction) { - case FocusNavigationDirection.Left: + case NavigationDirection.Left: newItemIndex = itemIndex - 1; break; - case FocusNavigationDirection.Right: + case NavigationDirection.Right: newItemIndex = itemIndex + 1; break; } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 7298535b15..151d8679cf 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -81,7 +81,7 @@ namespace Avalonia.Controls.Presenters } /// - IControl ILogicalScrollable.GetControlInDirection(FocusNavigationDirection direction, IControl from) + IControl ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from) { return _virtualizer?.GetControlInDirection(direction, from); } diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs index 3fb201affc..6c8f463a96 100644 --- a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs +++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs @@ -64,6 +64,6 @@ namespace Avalonia.Controls.Primitives /// The movement direction. /// The control from which movement begins. /// The control. - IControl GetControlInDirection(FocusNavigationDirection direction, IControl from); + IControl GetControlInDirection(NavigationDirection direction, IControl from); } } diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index c52b738d8b..f5b3c20d5e 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -72,7 +72,7 @@ namespace Avalonia.Controls /// The movement direction. /// The control from which movement begins. /// The control. - IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) { var fromControl = from as IControl; return (fromControl != null) ? GetControlInDirection(direction, fromControl) : null; @@ -84,35 +84,35 @@ namespace Avalonia.Controls /// The movement direction. /// The control from which movement begins. /// The control. - protected virtual IInputElement GetControlInDirection(FocusNavigationDirection direction, IControl from) + protected virtual IInputElement GetControlInDirection(NavigationDirection direction, IControl from) { var horiz = Orientation == Orientation.Horizontal; int index = Children.IndexOf((IControl)from); switch (direction) { - case FocusNavigationDirection.First: + case NavigationDirection.First: index = 0; break; - case FocusNavigationDirection.Last: + case NavigationDirection.Last: index = Children.Count - 1; break; - case FocusNavigationDirection.Next: + case NavigationDirection.Next: ++index; break; - case FocusNavigationDirection.Previous: + case NavigationDirection.Previous: --index; break; - case FocusNavigationDirection.Left: + case NavigationDirection.Left: index = horiz ? index - 1 : -1; break; - case FocusNavigationDirection.Right: + case NavigationDirection.Right: index = horiz ? index + 1 : -1; break; - case FocusNavigationDirection.Up: + case NavigationDirection.Up: index = horiz ? -1 : index - 1; break; - case FocusNavigationDirection.Down: + case NavigationDirection.Down: index = horiz ? -1 : index + 1; break; } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 0e40011760..46a17d601f 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -111,7 +111,7 @@ namespace Avalonia.Controls } } - protected override IInputElement GetControlInDirection(FocusNavigationDirection direction, IControl from) + protected override IInputElement GetControlInDirection(NavigationDirection direction, IControl from) { var logicalScrollable = Parent as ILogicalScrollable; var fromControl = from as IControl; diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index d0f1e93c56..9d3a5b6536 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -48,35 +48,35 @@ namespace Avalonia.Controls /// The movement direction. /// The control from which movement begins. /// The control. - IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) { var horiz = Orientation == Orientation.Horizontal; int index = Children.IndexOf((IControl)from); switch (direction) { - case FocusNavigationDirection.First: + case NavigationDirection.First: index = 0; break; - case FocusNavigationDirection.Last: + case NavigationDirection.Last: index = Children.Count - 1; break; - case FocusNavigationDirection.Next: + case NavigationDirection.Next: ++index; break; - case FocusNavigationDirection.Previous: + case NavigationDirection.Previous: --index; break; - case FocusNavigationDirection.Left: + case NavigationDirection.Left: index = horiz ? index - 1 : -1; break; - case FocusNavigationDirection.Right: + case NavigationDirection.Right: index = horiz ? index + 1 : -1; break; - case FocusNavigationDirection.Up: + case NavigationDirection.Up: index = horiz ? -1 : index - 1; break; - case FocusNavigationDirection.Down: + case NavigationDirection.Down: index = horiz ? -1 : index + 1; break; } diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj index 569e1bb2cc..3722c80def 100644 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ b/src/Avalonia.Input/Avalonia.Input.csproj @@ -1,4 +1,4 @@ - + @@ -79,7 +79,7 @@ - + diff --git a/src/Avalonia.Input/IKeyboardNavigationHandler.cs b/src/Avalonia.Input/IKeyboardNavigationHandler.cs index 14e2144da9..db3a3cf114 100644 --- a/src/Avalonia.Input/IKeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/IKeyboardNavigationHandler.cs @@ -25,7 +25,7 @@ namespace Avalonia.Input /// Any input modifiers active at the time of focus. void Move( IInputElement element, - FocusNavigationDirection direction, + NavigationDirection direction, InputModifiers modifiers = InputModifiers.None); } } \ No newline at end of file diff --git a/src/Avalonia.Input/INavigableContainer.cs b/src/Avalonia.Input/INavigableContainer.cs index 48f85819c0..13d734bd0b 100644 --- a/src/Avalonia.Input/INavigableContainer.cs +++ b/src/Avalonia.Input/INavigableContainer.cs @@ -14,6 +14,6 @@ namespace Avalonia.Input /// The movement direction. /// The control from which movement begins. /// The control. - IInputElement GetControl(FocusNavigationDirection direction, IInputElement from); + IInputElement GetControl(NavigationDirection direction, IInputElement from); } } diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 9991f7ec10..562f683079 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -48,11 +48,11 @@ namespace Avalonia.Input /// public static IInputElement GetNext( IInputElement element, - FocusNavigationDirection direction) + NavigationDirection direction) { Contract.Requires(element != null); - if (direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Previous) + if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) { return TabNavigation.GetNextInTabOrder(element, direction); } @@ -70,7 +70,7 @@ namespace Avalonia.Input /// Any input modifiers active at the time of focus. public void Move( IInputElement element, - FocusNavigationDirection direction, + NavigationDirection direction, InputModifiers modifiers = InputModifiers.None) { Contract.Requires(element != null); @@ -79,8 +79,8 @@ namespace Avalonia.Input if (next != null) { - var method = direction == FocusNavigationDirection.Next || - direction == FocusNavigationDirection.Previous ? + var method = direction == NavigationDirection.Next || + direction == NavigationDirection.Previous ? NavigationMethod.Tab : NavigationMethod.Directional; FocusManager.Instance.Focus(next, method, modifiers); } @@ -97,25 +97,25 @@ namespace Avalonia.Input if (current != null) { - FocusNavigationDirection? direction = null; + NavigationDirection? direction = null; switch (e.Key) { case Key.Tab: direction = (e.Modifiers & InputModifiers.Shift) == 0 ? - FocusNavigationDirection.Next : FocusNavigationDirection.Previous; + NavigationDirection.Next : NavigationDirection.Previous; break; case Key.Up: - direction = FocusNavigationDirection.Up; + direction = NavigationDirection.Up; break; case Key.Down: - direction = FocusNavigationDirection.Down; + direction = NavigationDirection.Down; break; case Key.Left: - direction = FocusNavigationDirection.Left; + direction = NavigationDirection.Left; break; case Key.Right: - direction = FocusNavigationDirection.Right; + direction = NavigationDirection.Right; break; } diff --git a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs index 4d6e706516..efd4539719 100644 --- a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs +++ b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs @@ -24,12 +24,12 @@ namespace Avalonia.Input.Navigation /// public static IInputElement GetNext( IInputElement element, - FocusNavigationDirection direction) + NavigationDirection direction) { Contract.Requires(element != null); Contract.Requires( - direction != FocusNavigationDirection.Next && - direction != FocusNavigationDirection.Previous); + direction != NavigationDirection.Next && + direction != NavigationDirection.Previous); var container = element.GetVisualParent(); @@ -63,12 +63,12 @@ namespace Avalonia.Input.Navigation /// /// The direction. /// True if the direction is forward. - private static bool IsForward(FocusNavigationDirection direction) + private static bool IsForward(NavigationDirection direction) { - return direction == FocusNavigationDirection.Next || - direction == FocusNavigationDirection.Last || - direction == FocusNavigationDirection.Right || - direction == FocusNavigationDirection.Down; + return direction == NavigationDirection.Next || + direction == NavigationDirection.Last || + direction == NavigationDirection.Right || + direction == NavigationDirection.Down; } /// @@ -77,7 +77,7 @@ namespace Avalonia.Input.Navigation /// The element. /// The direction to search. /// The element or null if not found.## - private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction) + private static IInputElement GetFocusableDescendent(IInputElement container, NavigationDirection direction) { return IsForward(direction) ? GetFocusableDescendents(container).FirstOrDefault() : @@ -121,9 +121,9 @@ namespace Avalonia.Input.Navigation private static IInputElement GetNextInContainer( IInputElement element, IInputElement container, - FocusNavigationDirection direction) + NavigationDirection direction) { - if (direction == FocusNavigationDirection.Down) + if (direction == NavigationDirection.Down) { var descendent = GetFocusableDescendents(element).FirstOrDefault(); @@ -156,7 +156,7 @@ namespace Avalonia.Input.Navigation element = null; } - if (element != null && direction == FocusNavigationDirection.Up) + if (element != null && direction == NavigationDirection.Up) { var descendent = GetFocusableDescendents(element).LastOrDefault(); @@ -180,7 +180,7 @@ namespace Avalonia.Input.Navigation /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer( IInputElement container, - FocusNavigationDirection direction) + NavigationDirection direction) { var parent = container.GetVisualParent(); var isForward = IsForward(direction); diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 90502cdbf9..bc3826d90e 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -24,12 +24,12 @@ namespace Avalonia.Input.Navigation /// public static IInputElement GetNextInTabOrder( IInputElement element, - FocusNavigationDirection direction) + NavigationDirection direction) { Contract.Requires(element != null); Contract.Requires( - direction == FocusNavigationDirection.Next || - direction == FocusNavigationDirection.Previous); + direction == NavigationDirection.Next || + direction == NavigationDirection.Previous); var container = element.GetVisualParent(); @@ -63,9 +63,9 @@ namespace Avalonia.Input.Navigation /// The element. /// The direction to search. /// The element or null if not found.## - private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction) + private static IInputElement GetFocusableDescendent(IInputElement container, NavigationDirection direction) { - return direction == FocusNavigationDirection.Next ? + return direction == NavigationDirection.Next ? GetFocusableDescendents(container).FirstOrDefault() : GetFocusableDescendents(container).LastOrDefault(); } @@ -128,9 +128,9 @@ namespace Avalonia.Input.Navigation private static IInputElement GetNextInContainer( IInputElement element, IInputElement container, - FocusNavigationDirection direction) + NavigationDirection direction) { - if (direction == FocusNavigationDirection.Next) + if (direction == NavigationDirection.Next) { var descendent = GetFocusableDescendents(element).FirstOrDefault(); @@ -165,7 +165,7 @@ namespace Avalonia.Input.Navigation element = null; } - if (element != null && direction == FocusNavigationDirection.Previous) + if (element != null && direction == NavigationDirection.Previous) { var descendent = GetFocusableDescendents(element).LastOrDefault(); @@ -189,14 +189,14 @@ namespace Avalonia.Input.Navigation /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer( IInputElement container, - FocusNavigationDirection direction) + NavigationDirection direction) { var parent = container.GetVisualParent(); IInputElement next = null; if (parent != null) { - if (direction == FocusNavigationDirection.Previous && parent.CanFocus()) + if (direction == NavigationDirection.Previous && parent.CanFocus()) { return parent; } @@ -204,7 +204,7 @@ namespace Avalonia.Input.Navigation var siblings = parent.GetVisualChildren() .OfType() .Where(FocusExtensions.CanFocusDescendents); - var sibling = direction == FocusNavigationDirection.Next ? + var sibling = direction == NavigationDirection.Next ? siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() : siblings.TakeWhile(x => x != container).LastOrDefault(); @@ -216,7 +216,7 @@ namespace Avalonia.Input.Navigation } else { - next = direction == FocusNavigationDirection.Next ? + next = direction == NavigationDirection.Next ? GetFocusableDescendents(sibling).FirstOrDefault() : GetFocusableDescendents(sibling).LastOrDefault(); } @@ -229,7 +229,7 @@ namespace Avalonia.Input.Navigation } else { - next = direction == FocusNavigationDirection.Next ? + next = direction == NavigationDirection.Next ? GetFocusableDescendents(container).FirstOrDefault() : GetFocusableDescendents(container).LastOrDefault(); } diff --git a/src/Avalonia.Input/FocusNavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs similarity index 78% rename from src/Avalonia.Input/FocusNavigationDirection.cs rename to src/Avalonia.Input/NavigationDirection.cs index c8f670a13c..fbaa7e74c7 100644 --- a/src/Avalonia.Input/FocusNavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -4,9 +4,9 @@ namespace Avalonia.Input { /// - /// Describes how focus should be moved. + /// Describes how focus should be moved by directional or tab keys. /// - public enum FocusNavigationDirection + public enum NavigationDirection { /// /// Move the focus to the next control in the tab order. @@ -47,5 +47,15 @@ namespace Avalonia.Input /// Move the focus down. /// Down, + + /// + /// Move the focus up a page. + /// + PageUp, + + /// + /// Move the focus down a page. + /// + PageDown, } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index f70915ad3c..5e7daacf60 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -384,7 +384,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[5]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Down, + NavigationDirection.Down, from); Assert.Same(target.Panel.Children[6], result); @@ -401,7 +401,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[9]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Down, + NavigationDirection.Down, from); Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); @@ -419,7 +419,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[8]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Down, + NavigationDirection.Down, from); Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); @@ -438,7 +438,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[1]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Up, + NavigationDirection.Up, from); Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); @@ -459,7 +459,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[5]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Right, + NavigationDirection.Right, from); Assert.Same(target.Panel.Children[6], result); @@ -476,7 +476,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[9]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Right, + NavigationDirection.Right, from); Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); @@ -494,7 +494,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[8]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Right, + NavigationDirection.Right, from); Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); @@ -513,7 +513,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[1]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Left, + NavigationDirection.Left, from); Assert.Equal(new Vector(10, 0), ((ILogicalScrollable)target).Offset); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs index 5a7e5f9f8b..1258a26d6a 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs @@ -353,7 +353,7 @@ namespace Avalonia.Controls.UnitTests return new Size(150, 150); } - public IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + public IControl GetControlInDirection(NavigationDirection direction, IControl from) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 8ed7f9d0bb..b8cd868252 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -161,9 +161,9 @@ namespace Avalonia.Controls.UnitTests scrollable.Setup(x => x.IsLogicalScrollEnabled).Returns(true); ((ISetLogicalParent)target).SetParent(presenter.Object); - ((INavigableContainer)target).GetControl(FocusNavigationDirection.Next, from); + ((INavigableContainer)target).GetControl(NavigationDirection.Next, from); - scrollable.Verify(x => x.GetControlInDirection(FocusNavigationDirection.Next, from)); + scrollable.Verify(x => x.GetControlInDirection(NavigationDirection.Next, from)); } } } diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs index c41162ffed..dddb120f26 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs @@ -44,7 +44,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -83,7 +83,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -113,7 +113,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -158,7 +158,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -177,7 +177,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(top, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(top, NavigationDirection.Down); Assert.Equal(next, result); } @@ -224,7 +224,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -262,7 +262,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -300,7 +300,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -339,7 +339,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -378,7 +378,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Null(result); } @@ -416,7 +416,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Null(result); } @@ -455,7 +455,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -494,7 +494,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -525,7 +525,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -570,7 +570,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -615,7 +615,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -635,7 +635,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(top, result); } @@ -674,7 +674,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -713,7 +713,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -752,7 +752,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -790,7 +790,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Null(result); } @@ -813,7 +813,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Null(result); } diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index cc77dba1fc..efc9abedbb 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -42,7 +42,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -79,7 +79,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -123,7 +123,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -152,7 +152,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -195,7 +195,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -213,7 +213,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(top, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(top, NavigationDirection.Next); Assert.Equal(next, result); } @@ -256,7 +256,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -294,7 +294,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -332,7 +332,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -370,7 +370,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -408,7 +408,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Null(result); } @@ -446,7 +446,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -486,7 +486,7 @@ namespace Avalonia.Input.UnitTests KeyboardNavigation.SetTabOnceActiveElement(container, next); - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -524,7 +524,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -564,7 +564,7 @@ namespace Avalonia.Input.UnitTests KeyboardNavigation.SetTabOnceActiveElement(container, next); - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -601,7 +601,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -638,7 +638,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -667,7 +667,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -710,7 +710,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -753,7 +753,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -772,7 +772,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(top, result); } @@ -810,7 +810,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -848,7 +848,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -886,7 +886,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -924,7 +924,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Null(result); } @@ -962,7 +962,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -1002,7 +1002,7 @@ namespace Avalonia.Input.UnitTests KeyboardNavigation.SetTabOnceActiveElement(container, next); - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -1040,7 +1040,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -1063,7 +1063,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Null(result); } From 6d5b3e02c4458ae75df2bf04fb2df9146aa90102 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 20:57:30 +0200 Subject: [PATCH 036/122] Added page up/down support to virt lists. --- .../Presenters/ItemVirtualizerSimple.cs | 27 ++++++++++++++++++- .../KeyboardNavigationHandler.cs | 6 +++++ .../Navigation/DirectionalNavigation.cs | 1 - 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index e9fbd33c72..2b65add3f7 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -7,6 +7,7 @@ using System.Collections.Specialized; using System.Linq; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Utilities; namespace Avalonia.Controls.Presenters { @@ -165,6 +166,12 @@ namespace Avalonia.Controls.Presenters case NavigationDirection.Down: newItemIndex = itemIndex + 1; break; + case NavigationDirection.PageUp: + newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); + break; + case NavigationDirection.PageDown: + newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); + break; } } else @@ -177,6 +184,12 @@ namespace Avalonia.Controls.Presenters case NavigationDirection.Right: newItemIndex = itemIndex + 1; break; + case NavigationDirection.PageUp: + newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); + break; + case NavigationDirection.PageDown: + newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); + break; } } @@ -189,7 +202,8 @@ namespace Avalonia.Controls.Presenters if (newItemIndex < firstIndex || newItemIndex > lastIndex) { - OffsetValue += newItemIndex - itemIndex; + var newOffset = OffsetValue + (newItemIndex - itemIndex); + OffsetValue = CoerceOffset(newOffset); InvalidateScroll(); } @@ -398,5 +412,16 @@ namespace Avalonia.Controls.Presenters Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count); NextIndex -= count; } + + /// + /// Ensures an offset value is within the value range. + /// + /// The value. + /// The coerced value. + private double CoerceOffset(double value) + { + var max = Math.Max(ExtentValue - ViewportValue, 0); + return MathUtilities.Clamp(value, 0, max); + } } } diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 562f683079..959e478141 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -117,6 +117,12 @@ namespace Avalonia.Input case Key.Right: direction = NavigationDirection.Right; break; + case Key.PageUp: + direction = NavigationDirection.PageUp; + break; + case Key.PageDown: + direction = NavigationDirection.PageDown; + break; } if (direction.HasValue) diff --git a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs index efd4539719..6d4da5d976 100644 --- a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs +++ b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs @@ -35,7 +35,6 @@ namespace Avalonia.Input.Navigation if (container != null) { - var isForward = IsForward(direction); var mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)container); switch (mode) From 5346f20d816206402064b7fa82081b31192a14a0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 15:43:11 +0200 Subject: [PATCH 037/122] Use correct ItemContainerGenerator for menus. --- src/Avalonia.Controls/Menu.cs | 7 +++++++ src/Avalonia.Controls/MenuItem.cs | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index d5d70ab1ea..2185fd982b 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Reactive.Disposables; +using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; @@ -131,6 +132,12 @@ namespace Avalonia.Controls _subscription.Dispose(); } + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); + } + /// /// Called when a key is pressed within the menu. /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index e4bb44049d..f73f2c755b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Windows.Input; +using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -204,6 +205,12 @@ namespace Avalonia.Controls IsSelected = true; } + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); + } + /// /// Called when a key is pressed in the . /// From 0d32bc420c96a2c620cc0245f72520c0ed1f9509 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 16:27:21 +0200 Subject: [PATCH 038/122] Allow changing orientation in virt sample. --- samples/VirtualizationTest/MainWindow.xaml | 7 +++++++ .../ViewModels/MainWindowViewModel.cs | 10 ++++++++++ .../Presenters/ItemVirtualizerSimple.cs | 5 ++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index 681de0a815..e8baecc28c 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -7,6 +7,8 @@ Gap="4"> + @@ -32,6 +34,11 @@ SelectedItems="{Binding SelectedItems}" SelectionMode="Multiple" VirtualizationMode="{Binding VirtualizationMode}"> + + + + + diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 3e07aa54e9..eb0a7bf01e 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -17,6 +17,7 @@ namespace VirtualizationTest.ViewModels private int _newItemIndex; private IReactiveList _items; private string _prefix = "Item"; + private Orientation _orientation; private ItemVirtualizationMode _virtualizationMode = ItemVirtualizationMode.Simple; public MainWindowViewModel() @@ -53,6 +54,15 @@ namespace VirtualizationTest.ViewModels private set { this.RaiseAndSetIfChanged(ref _items, value); } } + public Orientation Orientation + { + get { return _orientation; } + set { this.RaiseAndSetIfChanged(ref _orientation, value); } + } + + public IEnumerable Orientations => + Enum.GetValues(typeof(Orientation)).Cast(); + public ItemVirtualizationMode VirtualizationMode { get { return _virtualizationMode; } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 2b65add3f7..381080ce4a 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -72,7 +72,10 @@ namespace Avalonia.Controls.Presenters // offset the panel by the height of the first item. var firstIndex = ItemCount - panel.Children.Count; RecycleContainersForMove(firstIndex - FirstIndex); - panel.PixelOffset = panel.Children[0].Bounds.Height; + + panel.PixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ? + panel.Children[0].Bounds.Height : + panel.Children[0].Bounds.Width; } } } From ad9abd53c12142d585702a020e068d5731e0822e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 18:48:43 +0200 Subject: [PATCH 039/122] Fixed scrolling differently sized items. --- .../Presenters/ItemVirtualizerSimple.cs | 15 ++++++++++++++- src/Avalonia.SceneGraph/Rect.cs | 16 +++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 381080ce4a..f9b383a928 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -7,6 +7,7 @@ using System.Collections.Specialized; using System.Linq; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Utilities; namespace Avalonia.Controls.Presenters @@ -210,7 +211,19 @@ namespace Avalonia.Controls.Presenters InvalidateScroll(); } - return generator.ContainerFromIndex(newItemIndex); + var container = generator.ContainerFromIndex(newItemIndex); + + // We need to do a layout here because it's possible that the container we moved to + // is only partially visible due to differing item sizes. If the container is only + // partially visible, scroll again. + LayoutManager.Instance?.ExecuteLayoutPass(); + + if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) + { + OffsetValue += newItemIndex > itemIndex ? 1 : -1; + } + + return container; } return null; diff --git a/src/Avalonia.SceneGraph/Rect.cs b/src/Avalonia.SceneGraph/Rect.cs index de4d13890b..10253ec829 100644 --- a/src/Avalonia.SceneGraph/Rect.cs +++ b/src/Avalonia.SceneGraph/Rect.cs @@ -224,14 +224,24 @@ namespace Avalonia } /// - /// Determines whether a points in in the bounds of the rectangle. + /// Determines whether a point in in the bounds of the rectangle. /// /// The point. /// true if the point is in the bounds of the rectangle; otherwise false. public bool Contains(Point p) { - return p.X >= _x && p.X < _x + _width && - p.Y >= _y && p.Y < _y + _height; + return p.X >= _x && p.X <= _x + _width && + p.Y >= _y && p.Y <= _y + _height; + } + + /// + /// Determines whether the rectangle fully contains another rectangle. + /// + /// The rectangle. + /// true if the rectangle is fully contained; otherwise false. + public bool Contains(Rect r) + { + return Contains(r.TopLeft) && Contains(r.BottomRight); } /// From d7a757385a0e8c76ad4223c9204499645629977a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 20:15:53 +0200 Subject: [PATCH 040/122] Fix failing tests. --- .../Presenters/ItemVirtualizerSimple.cs | 15 ++++++++++----- .../ItemsPresenterTests_Virtualization_Simple.cs | 1 + 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index f9b383a928..e06b1749db 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -212,15 +212,20 @@ namespace Avalonia.Controls.Presenters } var container = generator.ContainerFromIndex(newItemIndex); + var layoutManager = LayoutManager.Instance; // We need to do a layout here because it's possible that the container we moved to // is only partially visible due to differing item sizes. If the container is only - // partially visible, scroll again. - LayoutManager.Instance?.ExecuteLayoutPass(); - - if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) + // partially visible, scroll again. Don't do this if there's no layout manager: + // it means we're running a unit test. + if (layoutManager != null) { - OffsetValue += newItemIndex > itemIndex ? 1 : -1; + layoutManager.ExecuteLayoutPass(); + + if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) + { + OffsetValue += newItemIndex > itemIndex ? 1 : -1; + } } return container; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 5e7daacf60..2afd3f4bc3 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -11,6 +11,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests.Presenters From 95eefa3cf1904d86358d81c1a06abd1c73cf29ca Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 22:24:16 +0200 Subject: [PATCH 041/122] Implement AutoScrollToSelectedItem For virtualizing lists. --- samples/VirtualizationTest/MainWindow.xaml | 2 + .../ViewModels/MainWindowViewModel.cs | 16 +++- .../Mixins/SelectableMixin.cs | 17 +--- .../Presenters/IItemsPresenter.cs | 2 + .../Presenters/ItemVirtualizer.cs | 9 +- .../Presenters/ItemVirtualizerSimple.cs | 95 ++++++++++++------- .../Presenters/ItemsPresenter.cs | 7 +- .../Presenters/ItemsPresenterBase.cs | 5 + .../Primitives/SelectingItemsControl.cs | 12 +++ ...emsPresenterTests_Virtualization_Simple.cs | 71 ++++++++------ 10 files changed, 148 insertions(+), 88 deletions(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index e8baecc28c..55bd729fec 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -27,6 +27,8 @@ + + Remove()); + + SelectFirstCommand = ReactiveCommand.Create(); + SelectFirstCommand.Subscribe(_ => SelectItem(0)); + + SelectLastCommand = ReactiveCommand.Create(); + SelectLastCommand.Subscribe(_ => SelectItem(Items.Count - 1)); } public string NewItemString @@ -73,10 +79,10 @@ namespace VirtualizationTest.ViewModels Enum.GetValues(typeof(ItemVirtualizationMode)).Cast(); public ReactiveCommand AddItemCommand { get; private set; } - public ReactiveCommand RecreateCommand { get; private set; } - public ReactiveCommand RemoveItemCommand { get; private set; } + public ReactiveCommand SelectFirstCommand { get; private set; } + public ReactiveCommand SelectLastCommand { get; private set; } private void ResizeItems(int count) { @@ -125,5 +131,11 @@ namespace VirtualizationTest.ViewModels .Select(x => new ItemViewModel(x, _prefix)); Items = new ReactiveList(items); } + + private void SelectItem(int index) + { + SelectedItems.Clear(); + SelectedItems.Add(Items[index]); + } } } diff --git a/src/Avalonia.Controls/Mixins/SelectableMixin.cs b/src/Avalonia.Controls/Mixins/SelectableMixin.cs index 0472b604db..2369e9f530 100644 --- a/src/Avalonia.Controls/Mixins/SelectableMixin.cs +++ b/src/Avalonia.Controls/Mixins/SelectableMixin.cs @@ -51,22 +51,7 @@ namespace Avalonia.Controls.Mixins if (sender != null) { - var itemsControl = sender.Parent as SelectingItemsControl; - - if ((bool)x.NewValue) - { - ((IPseudoClasses)sender.Classes).Add(":selected"); - - if (((IVisual)sender).IsAttachedToVisualTree && - itemsControl?.AutoScrollToSelectedItem == true) - { - sender.BringIntoView(); - } - } - else - { - ((IPseudoClasses)sender.Classes).Remove(":selected"); - } + ((IPseudoClasses)sender.Classes).Set(":selected", (bool)x.NewValue); sender.RaiseEvent(new RoutedEventArgs { diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 95df903ed3..42311dc781 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -6,5 +6,7 @@ namespace Avalonia.Controls.Presenters public interface IItemsPresenter : IPresenter { IPanel Panel { get; } + + void ScrollIntoView(object item); } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 4112c849d2..964ce82849 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -175,14 +175,11 @@ namespace Avalonia.Controls.Presenters } /// - /// Called when a request is made to bring an item into view. + /// Scrolls the specified item into view. /// - /// The item to bring into view. - /// The rect on the item to bring into view. - /// True if the request was handled; otherwise false. - public virtual bool BringIntoView(IVisual target, Rect targetRect) + /// The item. + public virtual void ScrollIntoView(object item) { - return false; } /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index e06b1749db..55e684015c 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -197,41 +197,18 @@ namespace Avalonia.Controls.Presenters } } - if (newItemIndex >= 0 && newItemIndex < ItemCount) - { - // Get the index of the first and last fully visible items (i.e. excluding any - // partially visible item at the beginning or end). - var firstIndex = panel.PixelOffset == 0 ? FirstIndex : FirstIndex + 1; - var lastIndex = (FirstIndex + ViewportValue) - 1; - - if (newItemIndex < firstIndex || newItemIndex > lastIndex) - { - var newOffset = OffsetValue + (newItemIndex - itemIndex); - OffsetValue = CoerceOffset(newOffset); - InvalidateScroll(); - } - - var container = generator.ContainerFromIndex(newItemIndex); - var layoutManager = LayoutManager.Instance; - - // We need to do a layout here because it's possible that the container we moved to - // is only partially visible due to differing item sizes. If the container is only - // partially visible, scroll again. Don't do this if there's no layout manager: - // it means we're running a unit test. - if (layoutManager != null) - { - layoutManager.ExecuteLayoutPass(); + return ScrollIntoView(newItemIndex); + } - if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) - { - OffsetValue += newItemIndex > itemIndex ? 1 : -1; - } - } + /// + public override void ScrollIntoView(object item) + { + var index = Items.IndexOf(item); - return container; + if (index != -1) + { + ScrollIntoView(index); } - - return null; } /// @@ -434,6 +411,60 @@ namespace Avalonia.Controls.Presenters NextIndex -= count; } + /// + /// Scrolls the item with the specified index into view. + /// + /// The item index. + /// The container that was brought into view. + private IControl ScrollIntoView(int index) + { + var panel = VirtualizingPanel; + var generator = Owner.ItemContainerGenerator; + var newOffset = -1.0; + + if (index >= 0 && index < ItemCount) + { + if (index < FirstIndex) + { + newOffset = index; + } + else if (index >= NextIndex) + { + newOffset = index - Math.Ceiling(ViewportValue - 1); + } + else if (OffsetValue + ViewportValue >= ItemCount) + { + newOffset = OffsetValue - 1; + } + + if (newOffset != -1) + { + OffsetValue = newOffset; + } + + var container = generator.ContainerFromIndex(index); + var layoutManager = LayoutManager.Instance; + + // We need to do a layout here because it's possible that the container we moved to + // is only partially visible due to differing item sizes. If the container is only + // partially visible, scroll again. Don't do this if there's no layout manager: + // it means we're running a unit test. + if (layoutManager != null) + { + layoutManager.ExecuteLayoutPass(); + + if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) + { + OffsetValue += 1; + } + } + + return container; + } + + return null; + } + /// /// Ensures an offset value is within the value range. /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 151d8679cf..4547c8fd45 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -77,7 +77,7 @@ namespace Avalonia.Controls.Presenters /// bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect) { - return _virtualizer?.BringIntoView(target, targetRect) ?? false; + return false; } /// @@ -86,6 +86,11 @@ namespace Avalonia.Controls.Presenters return _virtualizer?.GetControlInDirection(direction, from); } + public override void ScrollIntoView(object item) + { + _virtualizer?.ScrollIntoView(item); + } + /// protected override void PanelCreated(IPanel panel) { diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index abebe85080..5a56e52029 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -163,6 +163,11 @@ namespace Avalonia.Controls.Presenters } } + /// + public virtual void ScrollIntoView(object item) + { + } + /// /// Creates the for the control. /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a46aa8d853..27f3407fd9 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -280,6 +280,12 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Scrolls the specified item into view. + /// + /// The item. + public void ScrollIntoView(object item) => Presenter?.ScrollIntoView(item); + /// /// Tries to get the container that was the source of an event. /// @@ -723,6 +729,12 @@ namespace Avalonia.Controls.Primitives { case NotifyCollectionChangedAction.Add: SelectedItemsAdded(e.NewItems.Cast().ToList()); + + if (AutoScrollToSelectedItem) + { + ScrollIntoView(e.NewItems[0]); + } + added = e.NewItems; break; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 2afd3f4bc3..354f93097b 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -412,38 +412,44 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible() { - var target = CreateTarget(); + using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + { + var target = CreateTarget(); + var scroller = (ScrollContentPresenter)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 95)); - target.Arrange(new Rect(0, 0, 100, 95)); + scroller.Measure(new Size(100, 95)); + scroller.Arrange(new Rect(0, 0, 100, 95)); - var from = target.Panel.Children[8]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Down, - from); + var from = target.Panel.Children[8]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + NavigationDirection.Down, + from); - Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[8], result); + Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[8], result); + } } [Fact] public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() { - var target = CreateTarget(); + using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + { + var target = CreateTarget(); + var scroller = (ScrollContentPresenter)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 95)); - target.Arrange(new Rect(0, 0, 100, 95)); - ((ILogicalScrollable)target).Offset = new Vector(0, 11); + scroller.Measure(new Size(100, 95)); + scroller.Arrange(new Rect(0, 0, 100, 95)); + ((ILogicalScrollable)target).Offset = new Vector(0, 11); - var from = target.Panel.Children[1]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Up, - from); + var from = target.Panel.Children[1]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + NavigationDirection.Up, + from); - Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[0], result); + Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[0], result); + } } } @@ -487,19 +493,22 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Right_Should_Scroll_If_Partially_Visible() { - var target = CreateTarget(orientation: Orientation.Horizontal); + using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + { + var target = CreateTarget(orientation: Orientation.Horizontal); + var scroller = (ScrollContentPresenter)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(95, 100)); - target.Arrange(new Rect(0, 0, 95, 100)); + scroller.Measure(new Size(95, 100)); + scroller.Arrange(new Rect(0, 0, 95, 100)); - var from = target.Panel.Children[8]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Right, - from); + var from = target.Panel.Children[8]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + NavigationDirection.Right, + from); - Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[8], result); + Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[8], result); + } } [Fact] From 86dfadf514967e156c28fab72d19f72362d42be4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 23:12:13 +0200 Subject: [PATCH 042/122] Implement Home and End navigation. --- .../Presenters/ItemVirtualizerSimple.cs | 72 +++++++++++-------- .../KeyboardNavigationHandler.cs | 6 ++ src/Avalonia.Input/NavigationDirection.cs | 10 +++ 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 55e684015c..32e80e0566 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -152,6 +152,7 @@ namespace Avalonia.Controls.Presenters var generator = Owner.ItemContainerGenerator; var panel = VirtualizingPanel; var itemIndex = generator.IndexFromContainer(from); + var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical; if (itemIndex == -1) { @@ -160,41 +161,52 @@ namespace Avalonia.Controls.Presenters var newItemIndex = -1; - if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) + switch (direction) { - switch (direction) - { - case NavigationDirection.Up: + case NavigationDirection.Up: + if (vertical) + { newItemIndex = itemIndex - 1; - break; - case NavigationDirection.Down: + } + + break; + case NavigationDirection.Down: + if (vertical) + { newItemIndex = itemIndex + 1; - break; - case NavigationDirection.PageUp: - newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); - break; - case NavigationDirection.PageDown: - newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); - break; - } - } - else - { - switch (direction) - { - case NavigationDirection.Left: + } + + break; + + case NavigationDirection.Left: + if (!vertical) + { newItemIndex = itemIndex - 1; - break; - case NavigationDirection.Right: + } + break; + + case NavigationDirection.Right: + if (!vertical) + { newItemIndex = itemIndex + 1; - break; - case NavigationDirection.PageUp: - newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); - break; - case NavigationDirection.PageDown: - newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); - break; - } + } + break; + + case NavigationDirection.PageUp: + newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); + break; + + case NavigationDirection.PageDown: + newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); + break; + + case NavigationDirection.Home: + newItemIndex = 0; + break; + + case NavigationDirection.End: + newItemIndex = ItemCount - 1; + break; } return ScrollIntoView(newItemIndex); diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 959e478141..41c15986c7 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -123,6 +123,12 @@ namespace Avalonia.Input case Key.PageDown: direction = NavigationDirection.PageDown; break; + case Key.Home: + direction = NavigationDirection.Home; + break; + case Key.End: + direction = NavigationDirection.End; + break; } if (direction.HasValue) diff --git a/src/Avalonia.Input/NavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs index fbaa7e74c7..e04d08c6b2 100644 --- a/src/Avalonia.Input/NavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -57,5 +57,15 @@ namespace Avalonia.Input /// Move the focus down a page. /// PageDown, + + /// + /// Move the focus to the first item. + /// + Home, + + /// + /// Move the focus to the last item. + /// + End, } } From 22b495b1ac19900f0222caf6b92352503d0cf6ef Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 23:30:17 +0200 Subject: [PATCH 043/122] Fix directional nav for non-virtualized lists. Except Page Up/Down - these will need some extra code. --- .../Presenters/ItemVirtualizerNone.cs | 15 +++++++++++++++ .../Presenters/ItemVirtualizerSimple.cs | 16 ++++++++-------- src/Avalonia.Controls/StackPanel.cs | 3 +++ src/Avalonia.Input/KeyboardNavigationHandler.cs | 4 ++-- src/Avalonia.Input/NavigationDirection.cs | 10 ---------- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 959afe0065..56bb9299ae 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -110,6 +110,21 @@ namespace Avalonia.Controls.Presenters Owner.InvalidateMeasure(); } + /// + /// Scrolls the specified item into view. + /// + /// The item. + public override void ScrollIntoView(object item) + { + var index = Items.IndexOf(item); + + if (index != -1) + { + var container = Owner.ItemContainerGenerator.ContainerFromIndex(index); + container.BringIntoView(); + } + } + private IList AddContainers(int index, IEnumerable items) { var generator = Owner.ItemContainerGenerator; diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 32e80e0566..1dca52f885 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -163,6 +163,14 @@ namespace Avalonia.Controls.Presenters switch (direction) { + case NavigationDirection.First: + newItemIndex = 0; + break; + + case NavigationDirection.Last: + newItemIndex = ItemCount - 1; + break; + case NavigationDirection.Up: if (vertical) { @@ -199,14 +207,6 @@ namespace Avalonia.Controls.Presenters case NavigationDirection.PageDown: newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); break; - - case NavigationDirection.Home: - newItemIndex = 0; - break; - - case NavigationDirection.End: - newItemIndex = ItemCount - 1; - break; } return ScrollIntoView(newItemIndex); diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index f5b3c20d5e..26a755e5f1 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -115,6 +115,9 @@ namespace Avalonia.Controls case NavigationDirection.Down: index = horiz ? -1 : index + 1; break; + default: + index = -1; + break; } if (index >= 0 && index < Children.Count) diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 41c15986c7..57da49fa03 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -124,10 +124,10 @@ namespace Avalonia.Input direction = NavigationDirection.PageDown; break; case Key.Home: - direction = NavigationDirection.Home; + direction = NavigationDirection.First; break; case Key.End: - direction = NavigationDirection.End; + direction = NavigationDirection.Last; break; } diff --git a/src/Avalonia.Input/NavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs index e04d08c6b2..fbaa7e74c7 100644 --- a/src/Avalonia.Input/NavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -57,15 +57,5 @@ namespace Avalonia.Input /// Move the focus down a page. /// PageDown, - - /// - /// Move the focus to the first item. - /// - Home, - - /// - /// Move the focus to the last item. - /// - End, } } From 5ce57ad02974ebb41730532ac13e8fd0d59a6774 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 23:53:15 +0200 Subject: [PATCH 044/122] Fix menu separators. --- samples/ControlCatalog/Pages/MenuPage.xaml | 1 + .../Avalonia.Controls.csproj | 1 + .../Generators/MenuItemContainerGenerator.cs | 27 +++++++++++++++++++ src/Avalonia.Controls/MenuItem.cs | 2 +- 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index ca28fadea0..98171f29d6 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -10,6 +10,7 @@ + diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 47a56fcc82..c181aab1e2 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -51,6 +51,7 @@ + diff --git a/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs new file mode 100644 index 0000000000..c9b3a55aaa --- /dev/null +++ b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Controls.Generators +{ + public class MenuItemContainerGenerator : ItemContainerGenerator + { + /// + /// Initializes a new instance of the class. + /// + /// The owner control. + public MenuItemContainerGenerator(IControl owner) + : base(owner, MenuItem.HeaderProperty, null) + { + } + + /// + protected override IControl CreateContainer(object item) + { + var separator = item as Separator; + return separator != null ? separator : base.CreateContainer(item); + } + } +} diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index f73f2c755b..3d15ed99e7 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -208,7 +208,7 @@ namespace Avalonia.Controls /// protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); + return new MenuItemContainerGenerator(this); } /// From f24ab044f78edf98fb9bd69c2b624adbc5da1c1e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 00:26:00 +0200 Subject: [PATCH 045/122] Added failing binding tests. --- .../Data/BindingTests.cs | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index ca65b763f5..c5a1744810 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -1,11 +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 Moq; +using System; +using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.Data; +using Moq; using ReactiveUI; using Xunit; @@ -112,6 +114,24 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal("baz", source.Foo); } + [Fact] + public void Broken_Binding_Should_Use_Default_Value_Instead_Of_Inherited_Value() + { + var parent = new InheritanceTest() + { + Baz = 9, + DataContext = "data", + }; + + var child = new InheritanceTest(); + var bazBinding = new Binding("Missing"); + parent.Child = child; + + child.Bind(InheritanceTest.BazProperty, bazBinding); + + Assert.Equal(6, child.Baz); + } + [Fact] public void DataContext_Binding_Should_Use_Parent_DataContext() { @@ -159,6 +179,26 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal("foo", child.DataContext); } + [Fact] + public void DataContext_Binding_Should_Produce_Correct_Results() + { + var root = new Decorator + { + DataContext = new { Foo = "bar" }, + }; + + var child = new Control(); + var dataContextBinding = new Binding("Foo"); + var values = new List(); + + child.GetObservable(Border.DataContextProperty).Subscribe(x => values.Add(x)); + child.Bind(ContentControl.DataContextProperty, dataContextBinding); + + root.Child = child; + + Assert.Equal(new[] { null, "bar" }, values); + } + [Fact] public void Should_Use_DefaultValueConverter_When_No_Converter_Specified() { @@ -337,5 +377,17 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Bind(BarProperty, this.GetObservable(FooProperty)); } } + + private class InheritanceTest : Decorator + { + public static readonly StyledProperty BazProperty = + AvaloniaProperty.Register("Baz", defaultValue: 6, inherits: true); + + public int Baz + { + get { return GetValue(BazProperty); } + set { SetValue(BazProperty, value); } + } + } } } From bf02ae41a9050a2e2db21dadb9970d9248424f99 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 00:26:45 +0200 Subject: [PATCH 046/122] Revert "Set ContentPresenter.DataContext in UpdateChild." This reverts commit f3c7ea27a1e98dbcd777e27b9bc2a5eb1fffbdff. --- .../Presenters/ContentPresenter.cs | 92 ++++++++++--------- .../ItemsControlTests.cs | 1 - .../Presenters/ContentPresenterTests.cs | 4 +- .../Presenters/ItemsPresenterTests.cs | 1 - 4 files changed, 48 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 54d7e76d73..81ed48f5b1 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -96,6 +96,10 @@ namespace Avalonia.Controls.Presenters /// public ContentPresenter() { + var dataContext = this.GetObservable(ContentProperty) + .Select(x => x is IControl ? AvaloniaProperty.UnsetValue : x); + + Bind(Control.DataContextProperty, dataContext); } /// @@ -217,72 +221,70 @@ namespace Avalonia.Controls.Presenters /// public void UpdateChild() { + var old = Child; var content = Content; - var oldChild = Child; - var newChild = content as IControl; + var result = content as IControl; - if (content != null && newChild == null) + if (result == null) { - // We have content and it isn't a control, so first try to recycle the existing - // child control to display the new data by querying if the template that created - // the child can recycle items and that it also matches the new data. - if (oldChild != null && - _dataTemplate != null && - _dataTemplate.SupportsRecycling && - _dataTemplate.Match(content)) - { - newChild = oldChild; - } - else + DataContext = content; + + if (content != null) { - // We couldn't recycle an existing control so find a data template for the data - // and use it to create a control. - _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; - newChild = _dataTemplate.Build(content); + if (old != null && + _dataTemplate != null && + _dataTemplate.SupportsRecycling && + _dataTemplate.Match(content)) + { + result = old; + } + else + { + _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; + result = _dataTemplate.Build(content); - // Try to give the new control its own name scope. - var controlResult = newChild as Control; + var controlResult = result as Control; - if (controlResult != null) - { - NameScope.SetNameScope(controlResult, new NameScope()); + if (controlResult != null) + { + NameScope.SetNameScope(controlResult, new NameScope()); + } } } + else + { + _dataTemplate = null; + } } else { _dataTemplate = null; } - // Remove the old child if we're not recycling it. - if (oldChild != null && newChild != oldChild) + if (result != old) { - VisualChildren.Remove(oldChild); - } + if (old != null) + { + VisualChildren.Remove(old); + } - // Set the DataContext if the data isn't a control. - if (!(content is IControl)) - { - DataContext = content; - } + if (result != null) + { + ((ISetInheritanceParent)result).SetParent(this); - // Update the Child. - if (newChild == null) - { - Child = null; - } - else if (newChild != oldChild) - { - ((ISetInheritanceParent)newChild).SetParent(this); + Child = result; - Child = newChild; + if (result.Parent == null) + { + ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this); + } - if (newChild.Parent == null) + VisualChildren.Add(result); + } + else { - ((ISetLogicalParent)newChild).SetParent((ILogical)this.TemplatedParent ?? this); + Child = null; } - - VisualChildren.Add(newChild); } _createdChild = true; diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index dcbc71b9a1..ce2dc4ab6c 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -370,7 +370,6 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); var dataContexts = target.Presenter.Panel.Children - .Do(x => (x as ContentPresenter)?.UpdateChild()) .Cast() .Select(x => x.DataContext) .ToList(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs index 5c26ba5a5e..cd24631661 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs @@ -143,15 +143,13 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Assigning_NonControl_To_Content_Should_Set_DataContext_On_UpdateChild() + public void Assigning_NonControl_To_Content_Should_Set_DataContext() { var target = new ContentPresenter { Content = "foo", }; - target.UpdateChild(); - Assert.Equal("foo", target.DataContext); } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index da1558439f..9f76767ec1 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -312,7 +312,6 @@ namespace Avalonia.Controls.UnitTests.Presenters var dataContexts = target.Panel.Children .Cast() - .Do(x => x.UpdateChild()) .Select(x => x.DataContext) .ToList(); From ee37ab8df4c5d132e0aad015bfe97dda1c4e1dc2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 15 Jun 2016 19:18:06 +0300 Subject: [PATCH 047/122] Added UsePlatformDetect --- samples/BindingTest/App.xaml.cs | 3 +- samples/ControlCatalog.Desktop/Program.cs | 3 +- samples/TestApplication/Program.cs | 3 +- samples/XamlTestApplication/Program.cs | 3 +- .../Avalonia.Android/AndroidPlatform.cs | 1 - src/Avalonia.Controls/AppBuilder.cs | 46 ++++++++++++++++- src/Avalonia.Controls/Application.cs | 51 ------------------- .../DesignerAssist.cs | 4 +- 8 files changed, 50 insertions(+), 64 deletions(-) diff --git a/samples/BindingTest/App.xaml.cs b/samples/BindingTest/App.xaml.cs index fd457f7ba9..7bcaf837a8 100644 --- a/samples/BindingTest/App.xaml.cs +++ b/samples/BindingTest/App.xaml.cs @@ -19,8 +19,7 @@ namespace BindingTest InitializeLogging(); AppBuilder.Configure() - .UseWin32() - .UseDirect2D1() + .UsePlatformDetect() .Start(); } diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index ae8c9f5d8f..03842f78c1 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -17,8 +17,7 @@ namespace ControlCatalog // TODO: Make this work with GTK/Skia/Cairo depending on command-line args // again. AppBuilder.Configure() - .UseWin32() - .UseDirect2D1() + .UsePlatformDetect() .Start(); } diff --git a/samples/TestApplication/Program.cs b/samples/TestApplication/Program.cs index 5e79916a8d..9e331c8634 100644 --- a/samples/TestApplication/Program.cs +++ b/samples/TestApplication/Program.cs @@ -35,8 +35,7 @@ namespace TestApplication var app = new App(); AppBuilder.Configure(app) - .UseWin32() - .UseDirect2D1() + .UsePlatformDetect() .SetupWithoutStarting(); app.Run(); diff --git a/samples/XamlTestApplication/Program.cs b/samples/XamlTestApplication/Program.cs index 50b3c364d4..6485796ce7 100644 --- a/samples/XamlTestApplication/Program.cs +++ b/samples/XamlTestApplication/Program.cs @@ -21,8 +21,7 @@ namespace XamlTestApplication InitializeLogging(); AppBuilder.Configure() - .UseWin32() - .UseDirect2D1() + .UsePlatformDetect() .Start(); } diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 012e19da50..3192629494 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -38,7 +38,6 @@ namespace Avalonia.Android .Bind().ToConstant(this); SkiaPlatform.Initialize(); - Application.RegisterPlatformCallback(() => { }); _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity; diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index 86ba8c3786..8b3e0e731a 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reflection; namespace Avalonia.Controls { @@ -98,23 +99,64 @@ namespace Avalonia.Controls /// /// The method to call to initialize the windowing subsystem. /// An instance. - public AppBuilder WithWindowingSubsystem(Action initializer) + public AppBuilder UseWindowingSubsystem(Action initializer) { WindowingSubsystem = initializer; return this; } + /// + /// Specifies a windowing subsystem to use. + /// + /// The dll in which to look for subsystem. + /// An instance. + public AppBuilder UseWindowingSubsystem(string dll) => UseWindowingSubsystem(GetInitializer(dll)); + /// /// Specifies a rendering subsystem to use. /// /// The method to call to initialize the rendering subsystem. /// An instance. - public AppBuilder WithRenderingSubsystem(Action initializer) + public AppBuilder UseRenderingSubsystem(Action initializer) { RenderingSubsystem = initializer; return this; } + /// + /// Specifies a rendering subsystem to use. + /// + /// The dll in which to look for subsystem. + /// An instance. + public AppBuilder UseRenderingSubsystem(string dll) => UseRenderingSubsystem(GetInitializer(dll)); + + static Action GetInitializer(string assemblyName) => () => + { + var assembly = Assembly.Load(new AssemblyName(assemblyName)); + var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform"; + var platformClassFullName = assemblyName + "." + platformClassName; + var platformClass = assembly.GetType(platformClassFullName); + var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]); + init.Invoke(null, null); + }; + + public AppBuilder UsePlatformDetect() + { + var platformId = (int) + ((dynamic) Type.GetType("System.Environment").GetRuntimeProperty("OSVersion").GetValue(null)).Platform; + if (platformId == 4 || platformId == 6) + { + UseRenderingSubsystem("Avalonia.Cairo"); + UseWindowingSubsystem("Avalonia.Gtk"); + } + else + { + UseRenderingSubsystem("Avalonia.Direct2D1"); + UseWindowingSubsystem("Avalonia.Win32"); + } + return this; + } + /// /// Sets up the platform-speciic services for the . /// diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index f73ead5576..1991f49ac0 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -26,16 +26,12 @@ namespace Avalonia /// - A global set of . /// - A . /// - An . - /// - Loads and initializes rendering and windowing subsystems with - /// and . /// - Registers services needed by the rest of Avalonia in the /// method. /// - Tracks the lifetime of the application. /// public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IApplicationLifecycle { - static Action _platformInitializationCallback; - /// /// The application-global data templates. /// @@ -121,11 +117,6 @@ namespace Avalonia /// IStyleHost IStyleHost.StylingParent => null; - public static void RegisterPlatformCallback(Action cb) - { - _platformInitializationCallback = cb; - } - /// /// Initializes the application by loading XAML etc. /// @@ -189,47 +180,5 @@ namespace Avalonia .Bind().ToTransient() .Bind().ToConstant(this); } - - /// - /// Initializes the rendering and windowing subsystems according to platform. - /// - /// The value of Environment.OSVersion.Platform. - protected void InitializeSubsystems(int platformID) - { - if (_platformInitializationCallback != null) - { - _platformInitializationCallback(); - } - else if (platformID == 4 || platformID == 6) - { - InitializeSubsystem("Avalonia.Cairo"); - InitializeSubsystem("Avalonia.Gtk"); - } - else - { - InitializeSubsystem("Avalonia.Direct2D1"); - InitializeSubsystem("Avalonia.Win32"); - } - } - - /// - /// Initializes the rendering or windowing subsystem defined by the specified assemblt. - /// - /// The name of the assembly. - protected static void InitializeSubsystem(string assemblyName) - { - var assembly = Assembly.Load(new AssemblyName(assemblyName)); - var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform"; - var platformClassFullName = assemblyName + "." + platformClassName; - var platformClass = assembly.GetType(platformClassFullName); - var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]); - init.Invoke(null, null); - } - - internal static void InitializeWin32Subsystem() - { - InitializeSubsystem("Avalonia.Direct2D1"); - InitializeSubsystem("Avalonia.Win32"); - } } } diff --git a/src/Avalonia.DesignerSupport/DesignerAssist.cs b/src/Avalonia.DesignerSupport/DesignerAssist.cs index 64aa9106c7..7050d17840 100644 --- a/src/Avalonia.DesignerSupport/DesignerAssist.cs +++ b/src/Avalonia.DesignerSupport/DesignerAssist.cs @@ -60,8 +60,8 @@ namespace Avalonia.DesignerSupport } AppBuilder.Configure(app == null ? new DesignerApp() : (Application) Activator.CreateInstance(app.AsType())) - .WithWindowingSubsystem(Application.InitializeWin32Subsystem) - .WithRenderingSubsystem(() => { }) + .UseWindowingSubsystem("Avalonia.Win32") + .UseRenderingSubsystem("Avalonia.Direct2D1") .SetupWithoutStarting(); } From 7018b3c36d33052d7b95b7471789c625fa34c9d6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 22:54:14 +0200 Subject: [PATCH 048/122] Removed test. Decided that we shouldn't do this. --- .../Data/BindingTests.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index c5a1744810..1174b4ceb5 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -114,24 +114,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal("baz", source.Foo); } - [Fact] - public void Broken_Binding_Should_Use_Default_Value_Instead_Of_Inherited_Value() - { - var parent = new InheritanceTest() - { - Baz = 9, - DataContext = "data", - }; - - var child = new InheritanceTest(); - var bazBinding = new Binding("Missing"); - parent.Child = child; - - child.Bind(InheritanceTest.BazProperty, bazBinding); - - Assert.Equal(6, child.Baz); - } - [Fact] public void DataContext_Binding_Should_Use_Parent_DataContext() { From 0b28e10f212e7429593c3485309d2222eb6140e5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 22:56:38 +0200 Subject: [PATCH 049/122] Make broken DataContext bindings produce null. This prevents incorrect DataContexts cascading down to children when the DataContext binding is invalid, e.g. when things are being set up. --- .../Avalonia.Markup.Xaml/Data/Binding.cs | 15 +++++++++- .../MarkupExtensions/BindingExtension.cs | 2 +- .../Avalonia.Markup/Data/ExpressionSubject.cs | 28 ++++++++++++++++--- .../Data/ExpressionSubjectTests.cs | 8 +++--- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index 4b812e12f0..ec60695374 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -20,6 +20,7 @@ namespace Avalonia.Markup.Xaml.Data /// public Binding() { + FallbackValue = AvaloniaProperty.UnsetValue; } /// @@ -27,6 +28,7 @@ namespace Avalonia.Markup.Xaml.Data /// /// The binding path. public Binding(string path) + : this() { Path = path; } @@ -122,12 +124,23 @@ namespace Avalonia.Markup.Xaml.Data throw new NotSupportedException(); } + var fallback = FallbackValue; + + // If we're binding to DataContext and our fallback is UnsetValue then override + // the fallback value to null, as broken bindings to DataContext must reset the + // DataContext in order to not propagate incorrect DataContexts to child controls. + // See Avalonia.Markup.Xaml.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results. + if (targetProperty == Control.DataContextProperty && fallback == AvaloniaProperty.UnsetValue) + { + fallback = null; + } + var subject = new ExpressionSubject( observer, targetProperty?.PropertyType ?? typeof(object), + fallback, Converter ?? DefaultValueConverter.Instance, ConverterParameter, - FallbackValue, Priority); return new InstancedBinding(subject, Mode, Priority); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index a58a8614ee..70d3f7d161 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -36,7 +36,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public IValueConverter Converter { get; set; } public object ConverterParameter { get; set; } public string ElementName { get; set; } - public object FallbackValue { get; set; } + public object FallbackValue { get; set; } = AvaloniaProperty.UnsetValue; public BindingMode Mode { get; set; } public string Path { get; set; } public BindingPriority Priority { get; set; } = BindingPriority.LocalValue; diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs index 05bf818aad..0a3be26c18 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs @@ -41,16 +41,36 @@ namespace Avalonia.Markup.Data /// /// A parameter to pass to . /// + /// The binding priority. + public ExpressionSubject( + ExpressionObserver inner, + Type targetType, + IValueConverter converter, + object converterParameter = null, + BindingPriority priority = BindingPriority.LocalValue) + : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The type to convert the value to. /// /// The value to use when the binding is unable to produce a value. /// + /// The value converter to use. + /// + /// A parameter to pass to . + /// /// The binding priority. public ExpressionSubject( ExpressionObserver inner, - Type targetType, + Type targetType, + object fallbackValue, IValueConverter converter, object converterParameter = null, - object fallbackValue = null, BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(inner != null); @@ -117,7 +137,7 @@ namespace Avalonia.Markup.Data _inner.Expression, error.Exception.Message); - if (_fallbackValue != null) + if (_fallbackValue != AvaloniaProperty.UnsetValue) { if (TypeUtilities.TryConvert( type, @@ -162,7 +182,7 @@ namespace Avalonia.Markup.Data ConverterParameter, CultureInfo.CurrentUICulture); - if (_fallbackValue != null && + if (_fallbackValue != AvaloniaProperty.UnsetValue && (converted == AvaloniaProperty.UnsetValue || converted is BindingError)) { diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs index 9ee32149e0..8b763e7fb9 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs @@ -123,8 +123,8 @@ namespace Avalonia.Markup.UnitTests.Data var target = new ExpressionSubject( new ExpressionObserver(data, "DoubleValue"), typeof(string), - DefaultValueConverter.Instance, - fallbackValue: "9.8"); + "9.8", + DefaultValueConverter.Instance); target.OnNext("foo"); @@ -162,7 +162,7 @@ namespace Avalonia.Markup.UnitTests.Data new ExpressionObserver(data, "DoubleValue"), typeof(string), converter.Object, - "foo"); + converterParameter: "foo"); target.Subscribe(_ => { }); @@ -178,7 +178,7 @@ namespace Avalonia.Markup.UnitTests.Data new ExpressionObserver(data, "DoubleValue"), typeof(string), converter.Object, - "foo"); + converterParameter: "foo"); target.OnNext("bar"); From e252a35509b2e20e0fa19522ee1b97d4d90ff7ae Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 22:57:24 +0200 Subject: [PATCH 050/122] Don't delay bind to DataContext. DataContext bindings are special in that they return null instead of UnsetValue when broken, and it's important that they do that when things are being set up. --- .../Avalonia.Markup.Xaml/Context/PropertyAccessor.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs b/src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs index 20d9b07daf..e295292ba0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs @@ -136,7 +136,14 @@ namespace Avalonia.Markup.Xaml.Context if (control != null) { - DelayedBinding.Add(control, property, binding); + if (property != Control.DataContextProperty) + { + DelayedBinding.Add(control, property, binding); + } + else + { + control.Bind(property, binding); + } } else { From 7b3e25949e4b98a9aadb4f5fd2e97cd702b42776 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 22:58:10 +0200 Subject: [PATCH 051/122] Revert "Revert "Set ContentPresenter.DataContext in UpdateChild."" This reverts commit bf02ae41a9050a2e2db21dadb9970d9248424f99. --- .../Presenters/ContentPresenter.cs | 92 +++++++++---------- .../ItemsControlTests.cs | 1 + .../Presenters/ContentPresenterTests.cs | 4 +- .../Presenters/ItemsPresenterTests.cs | 1 + 4 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 81ed48f5b1..54d7e76d73 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -96,10 +96,6 @@ namespace Avalonia.Controls.Presenters /// public ContentPresenter() { - var dataContext = this.GetObservable(ContentProperty) - .Select(x => x is IControl ? AvaloniaProperty.UnsetValue : x); - - Bind(Control.DataContextProperty, dataContext); } /// @@ -221,39 +217,36 @@ namespace Avalonia.Controls.Presenters /// public void UpdateChild() { - var old = Child; var content = Content; - var result = content as IControl; + var oldChild = Child; + var newChild = content as IControl; - if (result == null) + if (content != null && newChild == null) { - DataContext = content; - - if (content != null) + // We have content and it isn't a control, so first try to recycle the existing + // child control to display the new data by querying if the template that created + // the child can recycle items and that it also matches the new data. + if (oldChild != null && + _dataTemplate != null && + _dataTemplate.SupportsRecycling && + _dataTemplate.Match(content)) { - if (old != null && - _dataTemplate != null && - _dataTemplate.SupportsRecycling && - _dataTemplate.Match(content)) - { - result = old; - } - else - { - _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; - result = _dataTemplate.Build(content); - - var controlResult = result as Control; - - if (controlResult != null) - { - NameScope.SetNameScope(controlResult, new NameScope()); - } - } + newChild = oldChild; } else { - _dataTemplate = null; + // We couldn't recycle an existing control so find a data template for the data + // and use it to create a control. + _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; + newChild = _dataTemplate.Build(content); + + // Try to give the new control its own name scope. + var controlResult = newChild as Control; + + if (controlResult != null) + { + NameScope.SetNameScope(controlResult, new NameScope()); + } } } else @@ -261,30 +254,35 @@ namespace Avalonia.Controls.Presenters _dataTemplate = null; } - if (result != old) + // Remove the old child if we're not recycling it. + if (oldChild != null && newChild != oldChild) { - if (old != null) - { - VisualChildren.Remove(old); - } + VisualChildren.Remove(oldChild); + } - if (result != null) - { - ((ISetInheritanceParent)result).SetParent(this); + // Set the DataContext if the data isn't a control. + if (!(content is IControl)) + { + DataContext = content; + } - Child = result; + // Update the Child. + if (newChild == null) + { + Child = null; + } + else if (newChild != oldChild) + { + ((ISetInheritanceParent)newChild).SetParent(this); - if (result.Parent == null) - { - ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this); - } + Child = newChild; - VisualChildren.Add(result); - } - else + if (newChild.Parent == null) { - Child = null; + ((ISetLogicalParent)newChild).SetParent((ILogical)this.TemplatedParent ?? this); } + + VisualChildren.Add(newChild); } _createdChild = true; diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index ce2dc4ab6c..dcbc71b9a1 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -370,6 +370,7 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); var dataContexts = target.Presenter.Panel.Children + .Do(x => (x as ContentPresenter)?.UpdateChild()) .Cast() .Select(x => x.DataContext) .ToList(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs index cd24631661..5c26ba5a5e 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs @@ -143,13 +143,15 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Assigning_NonControl_To_Content_Should_Set_DataContext() + public void Assigning_NonControl_To_Content_Should_Set_DataContext_On_UpdateChild() { var target = new ContentPresenter { Content = "foo", }; + target.UpdateChild(); + Assert.Equal("foo", target.DataContext); } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 9f76767ec1..da1558439f 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -312,6 +312,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var dataContexts = target.Panel.Children .Cast() + .Do(x => x.UpdateChild()) .Select(x => x.DataContext) .ToList(); From e1275eae56b2bda3af6e2898b198dde5ae7dcf8d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 23:29:16 +0200 Subject: [PATCH 052/122] Fixed .sln file. .sln files, you suck for merging. --- Avalonia.sln | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Avalonia.sln b/Avalonia.sln index 7a9c1f04f5..087e42af65 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -156,7 +156,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Avalonia.RenderTests", "tests\Avalonia.RenderTests\Avalonia.RenderTests.shproj", "{48840EDD-24BF-495D-911E-2EB12AE75D3B}" +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Aval+onia.RenderTests", "tests\Avalonia.RenderTests\Avalonia.RenderTests.shproj", "{48840EDD-24BF-495D-911E-2EB12AE75D3B}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}" EndProject Global From 69e2c459c4ce70d182357b4a0b540f3cdb7d526b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 16 Jun 2016 00:23:08 +0200 Subject: [PATCH 053/122] Do a null Items check in ScrollIntoView This can happen when bindings are in the process of updating, or if the client calls SelectingItemsControl.ScrollIntoView themselves. --- .../Presenters/ItemVirtualizerNone.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 56bb9299ae..411f309368 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -116,12 +116,15 @@ namespace Avalonia.Controls.Presenters /// The item. public override void ScrollIntoView(object item) { - var index = Items.IndexOf(item); - - if (index != -1) + if (Items != null) { - var container = Owner.ItemContainerGenerator.ContainerFromIndex(index); - container.BringIntoView(); + var index = Items.IndexOf(item); + + if (index != -1) + { + var container = Owner.ItemContainerGenerator.ContainerFromIndex(index); + container.BringIntoView(); + } } } From 99878ef8bb86bb837b2f1e6485093fb9dad677e1 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 16 Jun 2016 01:44:26 +0300 Subject: [PATCH 054/122] Designer proccess fixes --- .../AppHost/AvaloniaAppHost.cs | 87 +------------------ .../Avalonia.Designer.csproj | 7 +- .../AvaloniaDesigner.xaml.cs | 25 +++--- .../Avalonia.Designer/Comm/ProcessHost.cs | 13 +-- .../Avalonia.Designer/Comm/UpdateMetadata.cs | 20 ----- src/Windows/Avalonia.Designer/DemoWindow.xaml | 5 +- .../Avalonia.Designer/DemoWindow.xaml.cs | 21 +++++ .../Metadata/AvaloniaDesignerMetadata.cs | 51 ----------- 8 files changed, 47 insertions(+), 182 deletions(-) delete mode 100644 src/Windows/Avalonia.Designer/Comm/UpdateMetadata.cs delete mode 100644 src/Windows/Avalonia.Designer/Metadata/AvaloniaDesignerMetadata.cs diff --git a/src/Windows/Avalonia.Designer/AppHost/AvaloniaAppHost.cs b/src/Windows/Avalonia.Designer/AppHost/AvaloniaAppHost.cs index 442499c783..b1ad9f0f69 100644 --- a/src/Windows/Avalonia.Designer/AppHost/AvaloniaAppHost.cs +++ b/src/Windows/Avalonia.Designer/AppHost/AvaloniaAppHost.cs @@ -10,7 +10,6 @@ using System.Windows.Forms.Integration; using System.Xml; using Avalonia.Designer.Comm; using Avalonia.Designer.InProcDesigner; -using Avalonia.Designer.Metadata; using Timer = System.Windows.Forms.Timer; using Avalonia.DesignerSupport; @@ -91,88 +90,7 @@ namespace Avalonia.Designer.AppHost } } - AvaloniaDesignerMetadata BuildMetadata(List asms, Type xmlNsAttr) - { - var rv = new AvaloniaDesignerMetadata() - { - - NamespaceAliases = new List(), - Types = new List() - }; - - - foreach (var asm in asms) - { - foreach (dynamic xmlns in asm.GetCustomAttributes().Where(a => a.GetType() == xmlNsAttr)) - { - rv.NamespaceAliases.Add(new MetadataNamespaceAlias - { - Namespace = (string)xmlns.ClrNamespace, - XmlNamespace = (string)xmlns.XmlNamespace - }); - } - - try - { - foreach (var type in asm.GetTypes()) - { - try - { - if (!type.IsPublic || type.IsAbstract) - continue; - var t = new MetadataType() - { - Name = type.Name, - Namespace = type.Namespace, - Properties = new List() - }; - rv.Types.Add(t); - foreach (var prop in type.GetProperties()) - { - if (prop.GetMethod?.IsPublic != true) - continue; - var p = new MetadataProperty() - { - Name = prop.Name, - Type = - prop.PropertyType == typeof (string) || - (prop.PropertyType.IsValueType && - prop.PropertyType.Assembly == typeof (int).Assembly) - ? MetadataPropertyType.BasicType - : prop.PropertyType.IsEnum - ? MetadataPropertyType.Enum - : MetadataPropertyType.MetadataType - - }; - if (p.Type == MetadataPropertyType.Enum) - p.EnumValues = Enum.GetNames(prop.PropertyType); - if (p.Type == MetadataPropertyType.MetadataType) - p.MetadataFullTypeName = prop.PropertyType.Namespace + "." + prop.PropertyType.Name; - t.Properties.Add(p); - } - } - catch - { - // - } - } - } - catch - { - // - } - } - return rv; - } - - void BuildMetadataAndSendMessageAsync(List asms) - { - var xmlNsAttr = LookupType("Avalonia.Metadata.XmlnsDefinitionAttribute"); - new Thread(() => - { - _comm.SendMessage(new UpdateMetadataMessage(BuildMetadata(asms, xmlNsAttr))); - }).Start(); - } + private void DoInit(string targetExe, StringBuilder logger) { @@ -196,9 +114,6 @@ namespace Avalonia.Designer.AppHost logger.AppendLine(e.ToString()); } - log("Looking up Avalonia types"); - BuildMetadataAndSendMessageAsync(asms); - log("Initializing built-in designer"); var dic = new Dictionary(); Api = new DesignerApi(dic) {OnResize = OnResize, OnWindowCreated = OnWindowCreated}; diff --git a/src/Windows/Avalonia.Designer/Avalonia.Designer.csproj b/src/Windows/Avalonia.Designer/Avalonia.Designer.csproj index 5ab3b2e78d..a84cec6e5a 100644 --- a/src/Windows/Avalonia.Designer/Avalonia.Designer.csproj +++ b/src/Windows/Avalonia.Designer/Avalonia.Designer.csproj @@ -1,4 +1,4 @@ - + @@ -82,7 +82,6 @@ - @@ -91,7 +90,6 @@ InProcDesignerView.xaml - AvaloniaDesigner.xaml @@ -126,6 +124,9 @@ + + + "}, + new TextBlock {Text = ""}, + new TextBlock {Text = "before setters in your first Style"} + } + }; + } + else + control = (Control) loaded; + window = control as Window; if (window == null) { - window = new Window() {Content = original}; + window = new Window() {Content = (Control)control}; } if (!window.IsSet(Window.SizeToContentProperty)) @@ -114,7 +139,7 @@ namespace Avalonia.DesignerSupport s_currentWindow?.Close(); s_currentWindow = window; window.Show(); - Design.ApplyDesignerProperties(window, original); + Design.ApplyDesignerProperties(window, control); Api.OnWindowCreated?.Invoke(window.PlatformImpl.Handle.Handle); Api.OnResize?.Invoke(); } From f439c141db354cdc9e70d9533e7d399a589b4f2f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 16 Jun 2016 17:04:23 +0300 Subject: [PATCH 057/122] Don't try to preview App.xaml --- src/Avalonia.DesignerSupport/DesignerAssist.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.DesignerSupport/DesignerAssist.cs b/src/Avalonia.DesignerSupport/DesignerAssist.cs index 3f6a46020f..8787144665 100644 --- a/src/Avalonia.DesignerSupport/DesignerAssist.cs +++ b/src/Avalonia.DesignerSupport/DesignerAssist.cs @@ -115,7 +115,7 @@ namespace Avalonia.DesignerSupport { Children = { - new TextBlock {Text = "Styles can't be edited without Design.PreviewWith. Add"}, + new TextBlock {Text = "Styles can't be previewed without Design.PreviewWith. Add"}, new TextBlock {Text = ""}, new TextBlock {Text = " "}, new TextBlock {Text = ""}, @@ -123,6 +123,8 @@ namespace Avalonia.DesignerSupport } }; } + if (loaded is Application) + control = new TextBlock {Text = "Application can't be previewed in design view"}; else control = (Control) loaded; From 259144838cd7f68308a46efae0dca89e74bf431a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Jun 2016 00:49:49 +0200 Subject: [PATCH 058/122] Set item container properties with Style priority. So that they can be overridden by styles. --- .../Generators/ItemContainerGenerator.cs | 14 +++++--- .../Generators/ItemContainerGenerator`1.cs | 5 +-- .../Generators/TreeItemContainerGenerator.cs | 2 +- .../Templates/TreeDataTemplate.cs | 2 +- .../Generators/ItemContainerGeneratorTests.cs | 36 +++++++++++++++++++ 5 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 45f7dff49c..6bbf757106 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; namespace Avalonia.Controls.Generators { @@ -187,11 +188,16 @@ namespace Avalonia.Controls.Generators if (result == null) { - result = new ContentPresenter + result = new ContentPresenter(); + result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style); + + if (ItemTemplate != null) { - ContentTemplate = ItemTemplate, - Content = item, - }; + result.SetValue( + ContentPresenter.ContentTemplateProperty, + ItemTemplate, + BindingPriority.TemplatedParent); + } } return result; diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 35107a84fc..3aa2181cd4 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -5,6 +5,7 @@ using System; using System.Linq.Expressions; using System.Reflection; using Avalonia.Controls.Templates; +using Avalonia.Data; namespace Avalonia.Controls.Generators { @@ -62,10 +63,10 @@ namespace Avalonia.Controls.Generators if (ContentTemplateProperty != null) { - result.SetValue(ContentTemplateProperty, ItemTemplate); + result.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style); } - result.SetValue(ContentProperty, item); + result.SetValue(ContentProperty, item, BindingPriority.Style); if (!(item is IControl)) { diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index e26a4fb0d6..8a70aa7307 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -78,7 +78,7 @@ namespace Avalonia.Controls.Generators var template = GetTreeDataTemplate(item, ItemTemplate); var result = new T(); - result.SetValue(ContentProperty, template.Build(item)); + result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style); var itemsSelector = template.ItemsSelector(item); diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index a640f00c65..32e0e4f4cb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -43,7 +43,7 @@ namespace Avalonia.Markup.Xaml.Templates if (ItemsSource != null) { var obs = new ExpressionObserver(item, ItemsSource.Path); - return new InstancedBinding(obs); + return new InstancedBinding(obs, BindingPriority.Style); } return null; diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs index 5d2269e73c..01b550fb3b 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Linq; +using System.Reactive.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; +using Avalonia.Data; using Xunit; namespace Avalonia.Controls.UnitTests.Generators @@ -111,6 +113,40 @@ namespace Avalonia.Controls.UnitTests.Generators Assert.Equal(new[] { 0, 1 }, target.Containers.Select(x => x.Index)); } + [Fact] + public void Style_Binding_Should_Be_Able_To_Override_Content() + { + var owner = new Decorator(); + var target = new ItemContainerGenerator(owner); + var container = (ContentPresenter)target.Materialize(0, "foo", null).ContainerControl; + + Assert.Equal("foo", container.Content); + + container.Bind( + ContentPresenter.ContentProperty, + Observable.Never().StartWith("bar"), + BindingPriority.Style); + + Assert.Equal("bar", container.Content); + } + + [Fact] + public void Style_Binding_Should_Be_Able_To_Override_Content_Typed() + { + var owner = new Decorator(); + var target = new ItemContainerGenerator(owner, ListBoxItem.ContentProperty, null); + var container = (ListBoxItem)target.Materialize(0, "foo", null).ContainerControl; + + Assert.Equal("foo", container.Content); + + container.Bind( + ContentPresenter.ContentProperty, + Observable.Never().StartWith("bar"), + BindingPriority.Style); + + Assert.Equal("bar", container.Content); + } + private IList Materialize( IItemContainerGenerator generator, int index, From d772017768fd952c38ba9ae2d89ac323739b2c02 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Jun 2016 01:28:32 +0200 Subject: [PATCH 059/122] Added