diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..c5a719ce90 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +open_collective: avalonia diff --git a/.gitignore b/.gitignore index 2b2c9c3d0d..971c945246 100644 --- a/.gitignore +++ b/.gitignore @@ -196,3 +196,5 @@ ModuleCache.noindex/ Build/Intermediates.noindex/ info.plist build-intermediate +obj-Direct2D1/ +obj-Skia/ diff --git a/.gitmodules b/.gitmodules index 22a241f120..10c780c09f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github"] - path = src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github - url = https://github.com/AvaloniaUI/Portable.Xaml.git [submodule "nukebuild/Numerge"] path = nukebuild/Numerge url = https://github.com/kekekeks/Numerge.git diff --git a/Avalonia.sln b/Avalonia.sln index 484d7a4cde..f86c18ba1e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -146,7 +146,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Serilog.props = build\Serilog.props build\SharpDX.props = build\SharpDX.props build\SkiaSharp.props = build\SkiaSharp.props - build\Splat.props = build\Splat.props build\System.Memory.props = build\System.Memory.props build\XUnit.props = build\XUnit.props EndProjectSection diff --git a/dirs.proj b/dirs.proj index 4939a158bb..e56320e73f 100644 --- a/dirs.proj +++ b/dirs.proj @@ -8,10 +8,11 @@ - - + + + diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 4b814a9cfb..a35f4f3eeb 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -144,6 +144,7 @@ enum AvnStandardCursorType CursorDragMove, CursorDragCopy, CursorDragLink, + CursorNone }; enum AvnWindowEdge diff --git a/native/Avalonia.Native/src/OSX/cursor.h b/native/Avalonia.Native/src/OSX/cursor.h index a8eb49c0b9..cfe91955d8 100644 --- a/native/Avalonia.Native/src/OSX/cursor.h +++ b/native/Avalonia.Native/src/OSX/cursor.h @@ -11,18 +11,24 @@ class Cursor : public ComSingleObject { private: NSCursor * _native; - + bool _isHidden; public: FORWARD_IUNKNOWN() - Cursor(NSCursor * cursor) + Cursor(NSCursor * cursor, bool isHidden = false) { _native = cursor; + _isHidden = isHidden; } NSCursor* GetNative() { return _native; } + + bool IsHidden () + { + return _isHidden; + } }; extern std::map s_cursorMap; diff --git a/native/Avalonia.Native/src/OSX/cursor.mm b/native/Avalonia.Native/src/OSX/cursor.mm index bd2c94a4d8..799fa9e8e6 100644 --- a/native/Avalonia.Native/src/OSX/cursor.mm +++ b/native/Avalonia.Native/src/OSX/cursor.mm @@ -21,6 +21,7 @@ class CursorFactory : public ComSingleObject s_cursorMap = { @@ -46,11 +47,13 @@ class CursorFactory : public ComSingleObject(cursor); this->cursor = avnCursor->GetNative(); UpdateCursor(); + + if(avnCursor->IsHidden()) + { + [NSCursor hide]; + } + else + { + [NSCursor unhide]; + } + return S_OK; } } diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 489cb228aa..2c5a09bee7 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -1,6 +1,7 @@  netstandard2.0;net461;netcoreapp2.0 + Avalonia diff --git a/readme.md b/readme.md index 12f683bd55..cf995f10fb 100644 --- a/readme.md +++ b/readme.md @@ -8,9 +8,9 @@ ## About -Avalonia is a WPF-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of OSs: Windows (.NET Framework, .NET Core), Linux (GTK), MacOS, Android and iOS. +Avalonia is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of OSs: Windows (.NET Framework, .NET Core), Linux (libX11), MacOS, Android (experimental) and iOS (exprerimental). -**Avalonia is currently in beta** which means that the framework is generally usable for writing applications, but there may be some bugs and breaking changes as we continue development. +**Avalonia is currently in beta** which means that the framework is generally usable for writing applications, but there may be some bugs and breaking changes as we continue development, for more details about the status see https://github.com/AvaloniaUI/Avalonia/issues/2239 | Control catalog | Desktop platforms | Mobile platforms | |---|---|---| diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 804ca1f9b8..6a40f7187d 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -11,6 +11,7 @@ + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 93f5611ec4..4027c5cd63 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -46,6 +46,12 @@ namespace ControlCatalog.NetCore public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() + .With(new X11PlatformOptions {EnableMultiTouch = true}) + .With(new Win32PlatformOptions + { + EnableMultitouch = true, + AllowEglInitialization = true + }) .UseSkia() .UseReactiveUI(); diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 9f1899acc5..1cddb9d295 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -31,6 +31,7 @@ + diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index 268b06f756..d1742c12b7 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -11,7 +11,7 @@ - + DataGrid A control for displaying and interacting with a data source. @@ -52,4 +52,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs new file mode 100644 index 0000000000..a1359519e6 --- /dev/null +++ b/samples/ControlCatalog/Pages/PointersPage.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace ControlCatalog.Pages +{ + public class PointersPage : Control + { + class PointerInfo + { + public Point Point { get; set; } + public Color Color { get; set; } + } + + private static Color[] AllColors = new[] + { + Colors.Aqua, + Colors.Beige, + Colors.Chartreuse, + Colors.Coral, + Colors.Fuchsia, + Colors.Crimson, + Colors.Lavender, + Colors.Orange, + Colors.Orchid, + Colors.ForestGreen, + Colors.SteelBlue, + Colors.PapayaWhip, + Colors.PaleVioletRed, + Colors.Goldenrod, + Colors.Maroon, + Colors.Moccasin, + Colors.Navy, + Colors.Wheat, + Colors.Violet, + Colors.Sienna, + Colors.Indigo, + Colors.Honeydew + }; + + private Dictionary _pointers = new Dictionary(); + + public PointersPage() + { + ClipToBounds = true; + } + + void UpdatePointer(PointerEventArgs e) + { + if (!_pointers.TryGetValue(e.Pointer, out var info)) + { + if (e.RoutedEvent == PointerMovedEvent) + return; + var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray(); + var color = colors[new Random().Next(0, colors.Length - 1)]; + _pointers[e.Pointer] = info = new PointerInfo {Color = color}; + } + + info.Point = e.GetPosition(this); + InvalidateVisual(); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + UpdatePointer(e); + e.Pointer.Capture(this); + base.OnPointerPressed(e); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + UpdatePointer(e); + base.OnPointerMoved(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + _pointers.Remove(e.Pointer); + InvalidateVisual(); + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size)); + foreach (var pt in _pointers.Values) + { + var brush = new ImmutableSolidColorBrush(pt.Color); + context.DrawGeometry(brush, null, new EllipseGeometry(new Rect(pt.Point.X - 75, pt.Point.Y - 75, + 150, 150))); + } + + } + } +} diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index 0c0a4d705b..64118a00b4 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -26,6 +26,10 @@ + + diff --git a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs index 038f3574cc..88b1bf0b6b 100644 --- a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Reactive; +using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls; using ReactiveUI; @@ -11,7 +12,7 @@ namespace ControlCatalog.ViewModels public MenuPageViewModel() { OpenCommand = ReactiveCommand.CreateFromTask(Open); - SaveCommand = ReactiveCommand.Create(Save); + SaveCommand = ReactiveCommand.Create(Save, Observable.Return(false)); OpenRecentCommand = ReactiveCommand.Create(OpenRecent); MenuItems = new[] diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 344250ee2b..7f63e7725f 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -38,6 +38,9 @@ + + + diff --git a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs new file mode 100644 index 0000000000..3eb2276c48 --- /dev/null +++ b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs @@ -0,0 +1,49 @@ +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using Avalonia.Visuals.Media.Imaging; + +namespace RenderDemo.Pages +{ + public class RenderTargetBitmapPage : Control + { + private RenderTargetBitmap _bitmap; + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _bitmap = new RenderTargetBitmap(new PixelSize(200, 200), new Vector(96, 96)); + base.OnAttachedToLogicalTree(e); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _bitmap.Dispose(); + _bitmap = null; + base.OnDetachedFromLogicalTree(e); + } + + readonly Stopwatch _st = Stopwatch.StartNew(); + public override void Render(DrawingContext context) + { + using (var ctxi = _bitmap.CreateDrawingContext(null)) + using(var ctx = new DrawingContext(ctxi, false)) + using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100) + * Matrix.CreateRotation(_st.Elapsed.TotalSeconds) + * Matrix.CreateTranslation(100, 100))) + { + ctxi.Clear(default); + ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100)); + } + + context.DrawImage(_bitmap, 1, + new Rect(0, 0, 200, 200), + new Rect(0, 0, 200, 200)); + Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); + base.Render(context); + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs index 112925ab0f..463d499aad 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs @@ -33,7 +33,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers return null; } - RawMouseEventType? mouseEventType = null; + RawPointerEventType? mouseEventType = null; var eventTime = DateTime.Now; //Basic touch support switch (e.Action) @@ -42,17 +42,17 @@ namespace Avalonia.Android.Platform.Specific.Helpers //may be bot flood the evnt system with too many event especially on not so powerfull mobile devices if ((eventTime - _lastTouchMoveEventTime).TotalMilliseconds > 10) { - mouseEventType = RawMouseEventType.Move; + mouseEventType = RawPointerEventType.Move; } break; case MotionEventActions.Down: - mouseEventType = RawMouseEventType.LeftButtonDown; + mouseEventType = RawPointerEventType.LeftButtonDown; break; case MotionEventActions.Up: - mouseEventType = RawMouseEventType.LeftButtonUp; + mouseEventType = RawPointerEventType.LeftButtonUp; break; } @@ -75,14 +75,14 @@ namespace Avalonia.Android.Platform.Specific.Helpers //we need to generate mouse move before first mouse down event //as this is the way buttons are working every time //otherwise there is a problem sometimes - if (mouseEventType == RawMouseEventType.LeftButtonDown) + if (mouseEventType == RawPointerEventType.LeftButtonDown) { - var me = new RawMouseEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot, - RawMouseEventType.Move, _point, InputModifiers.None); + var me = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot, + RawPointerEventType.Move, _point, InputModifiers.None); _view.Input(me); } - var mouseEvent = new RawMouseEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot, + var mouseEvent = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot, mouseEventType.Value, _point, InputModifiers.LeftMouseButton); _view.Input(mouseEvent); diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index dcb3ef4a2b..41b57b6e70 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.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 System; +using System.Runtime.InteropServices; + namespace Avalonia.Utilities { /// @@ -8,6 +11,89 @@ namespace Avalonia.Utilities /// public static class MathUtilities { + /// + /// AreClose - Returns whether or not two doubles are "close". That is, whether or + /// not they are within epsilon of each other. + /// + /// The first double to compare. + /// The second double to compare. + public static bool AreClose(double value1, double value2) + { + //in case they are Infinities (then epsilon check does not work) + if (value1 == value2) return true; + double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon; + double delta = value1 - value2; + return (-eps < delta) && (eps > delta); + } + + /// + /// LessThan - Returns whether or not the first double is less than the second double. + /// That is, whether or not the first is strictly less than *and* not within epsilon of + /// the other number. + /// + /// The first double to compare. + /// The second double to compare. + public static bool LessThan(double value1, double value2) + { + return (value1 < value2) && !AreClose(value1, value2); + } + + /// + /// GreaterThan - Returns whether or not the first double is greater than the second double. + /// That is, whether or not the first is strictly greater than *and* not within epsilon of + /// the other number. + /// + /// The first double to compare. + /// The second double to compare. + public static bool GreaterThan(double value1, double value2) + { + return (value1 > value2) && !AreClose(value1, value2); + } + + /// + /// LessThanOrClose - Returns whether or not the first double is less than or close to + /// the second double. That is, whether or not the first is strictly less than or within + /// epsilon of the other number. + /// + /// The first double to compare. + /// The second double to compare. + public static bool LessThanOrClose(double value1, double value2) + { + return (value1 < value2) || AreClose(value1, value2); + } + + /// + /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to + /// the second double. That is, whether or not the first is strictly greater than or within + /// epsilon of the other number. + /// + /// The first double to compare. + /// The second double to compare. + public static bool GreaterThanOrClose(double value1, double value2) + { + return (value1 > value2) || AreClose(value1, value2); + } + + /// + /// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), + /// but this is faster. + /// + /// The double to compare to 1. + public static bool IsOne(double value) + { + return Math.Abs(value - 1.0) < 10.0 * double.Epsilon; + } + + /// + /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0), + /// but this is faster. + /// + /// The double to compare to 0. + public static bool IsZero(double value) + { + return Math.Abs(value) < 10.0 * double.Epsilon; + } + /// /// Clamps a value between a minimum and maximum value. /// @@ -31,6 +117,39 @@ namespace Avalonia.Utilities } } + /// + /// Calculates the value to be used for layout rounding at high DPI. + /// + /// Input value to be rounded. + /// Ratio of screen's DPI to layout DPI + /// Adjusted value that will produce layout rounding on screen at high dpi. + /// This is a layout helper method. It takes DPI into account and also does not return + /// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper associated with + /// UseLayoutRounding property and should not be used as a general rounding utility. + public static double RoundLayoutValue(double value, double dpiScale) + { + double newValue; + + // If DPI == 1, don't use DPI-aware rounding. + if (!MathUtilities.AreClose(dpiScale, 1.0)) + { + newValue = Math.Round(value * dpiScale) / dpiScale; + // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. + if (double.IsNaN(newValue) || + double.IsInfinity(newValue) || + MathUtilities.AreClose(newValue, double.MaxValue)) + { + newValue = value; + } + } + else + { + newValue = Math.Round(value); + } + + return newValue; + } + /// /// Clamps a value between a minimum and maximum value. /// diff --git a/src/Avalonia.Build.Tasks/Program.cs b/src/Avalonia.Build.Tasks/Program.cs index c2d0950264..d356b15408 100644 --- a/src/Avalonia.Build.Tasks/Program.cs +++ b/src/Avalonia.Build.Tasks/Program.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.IO; +using System.Linq; using Microsoft.Build.Framework; namespace Avalonia.Build.Tasks @@ -11,8 +12,14 @@ namespace Avalonia.Build.Tasks { if (args.Length != 3) { - Console.Error.WriteLine("input references output"); - return 1; + if (args.Length == 1) + args = new[] {"original.dll", "references", "out.dll"} + .Select(x => Path.Combine(args[0], x)).ToArray(); + else + { + Console.Error.WriteLine("input references output"); + return 1; + } } return new CompileAvaloniaXamlTask() diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs index 05193172be..f94f10f792 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs @@ -4,6 +4,7 @@ using System.Linq; using Avalonia.Utilities; using Mono.Cecil; using Mono.Cecil.Cil; +using Mono.Collections.Generic; using XamlIl.TypeSystem; namespace Avalonia.Build.Tasks @@ -144,6 +145,37 @@ namespace Avalonia.Build.Tasks }); } + + + private static bool MatchThisCall(Collection instructions, int idx) + { + var i = instructions[idx]; + // A "normal" way of passing `this` to a static method: + + // ldarg.0 + // call void [Avalonia.Markup.Xaml]Avalonia.Markup.Xaml.AvaloniaXamlLoader::Load(object) + + if (i.OpCode == OpCodes.Ldarg_0 || (i.OpCode == OpCodes.Ldarg && i.Operand?.Equals(0) == true)) + return true; + + /* F# way of using `this` in constructor emits a monstrosity like this: + IL_01c7: ldarg.0 + IL_01c8: ldfld class [FSharp.Core]Microsoft.FSharp.Core.FSharpRef`1 FVim.Cursor::this + IL_01cd: call instance !0 class [FSharp.Core]Microsoft.FSharp.Core.FSharpRef`1::get_contents() + IL_01d2: call !!0 [FSharp.Core]Microsoft.FSharp.Core.LanguagePrimitives/IntrinsicFunctions::CheckThis(!!0) + IL_01d7: call void [Avalonia.Markup.Xaml]Avalonia.Markup.Xaml.AvaloniaXamlLoader::Load(object) + + We check for the previous call to be Microsoft.FSharp.Core.LanguagePrimitives/IntrinsicFunctions::CheckThis + since it actually returns `this` + */ + if (i.OpCode == OpCodes.Call + && i.Operand is GenericInstanceMethod gim + && gim.Name == "CheckThis" + && gim.DeclaringType.FullName == "Microsoft.FSharp.Core.LanguagePrimitives/IntrinsicFunctions") + return true; + + return false; + } } } diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index ec668f2a2b..c54d8e19a1 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -150,7 +150,8 @@ namespace Avalonia.Build.Tasks classType = typeSystem.TargetAssembly.FindType(tn.Text); if (classType == null) throw new XamlIlParseException($"Unable to find type `{tn.Text}`", classDirective); - initialRoot.Type = new XamlIlAstClrTypeReference(classDirective, classType, false); + compiler.OverrideRootType(parsed, + new XamlIlAstClrTypeReference(classDirective, classType, false)); initialRoot.Children.Remove(classDirective); } @@ -233,8 +234,7 @@ namespace Avalonia.Build.Tasks var i = method.Body.Instructions; for (var c = 1; c < i.Count; c++) { - if (i[c - 1].OpCode == OpCodes.Ldarg_0 - && i[c].OpCode == OpCodes.Call) + if (i[c].OpCode == OpCodes.Call) { var op = i[c].Operand as MethodReference; @@ -253,8 +253,11 @@ namespace Avalonia.Build.Tasks && op.Parameters[0].ParameterType.FullName == "System.Object" && op.DeclaringType.FullName == "Avalonia.Markup.Xaml.AvaloniaXamlLoader") { - i[c].Operand = trampoline; - foundXamlLoader = true; + if (MatchThisCall(i, c - 1)) + { + i[c].Operand = trampoline; + foundXamlLoader = true; + } } } } diff --git a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj index 21d34ae4d6..27853a1540 100644 --- a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj +++ b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj @@ -1,6 +1,7 @@  netstandard2.0 + Avalonia.Controls.DataGrid diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index 8bc76543ba..04a1575486 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -881,10 +881,10 @@ namespace Avalonia.Controls && (double.IsNaN(_detailsContent.Height)) && (AreDetailsVisible) && (!double.IsNaN(_detailsDesiredHeight)) - && !DoubleUtil.AreClose(_detailsContent.Bounds.Height, _detailsDesiredHeight) + && !DoubleUtil.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight) && Slot != -1) { - _detailsDesiredHeight = _detailsContent.Bounds.Height; + _detailsDesiredHeight = _detailsContent.Bounds.Inflate(_detailsContent.Margin).Height; if (true) { @@ -943,6 +943,16 @@ namespace Avalonia.Controls _previousDetailsHeight = newValue.Height; } } + private void DetailsContent_BoundsChanged(Rect newValue) + { + if(_detailsContent != null) + DetailsContent_SizeChanged(newValue.Inflate(_detailsContent.Margin)); + } + private void DetailsContent_MarginChanged(Thickness newValue) + { + if (_detailsContent != null) + DetailsContent_SizeChanged(_detailsContent.Bounds.Inflate(newValue)); + } //TODO Animation // Sets AreDetailsVisible on the row and animates if necessary @@ -997,7 +1007,7 @@ namespace Avalonia.Controls } } } - + internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight) { if (_detailsElement != null && AreDetailsVisible) @@ -1023,8 +1033,11 @@ namespace Avalonia.Controls if (_detailsContent != null) { _detailsContentSizeSubscription = - _detailsContent.GetObservable(BoundsProperty) - .Subscribe(DetailsContent_SizeChanged); + System.Reactive.Disposables.StableCompositeDisposable.Create( + _detailsContent.GetObservable(BoundsProperty) + .Subscribe(DetailsContent_BoundsChanged), + _detailsContent.GetObservable(MarginProperty) + .Subscribe(DetailsContent_MarginChanged)); _detailsElement.Children.Add(_detailsContent); } } @@ -1053,4 +1066,4 @@ namespace Avalonia.Controls //TODO Styles -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index bbea3693cc..9f9fe5eae1 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -255,16 +255,13 @@ namespace Avalonia if (MainWindow == null) { - Dispatcher.UIThread.Post(() => + if (!mainWindow.IsVisible) { - if (!mainWindow.IsVisible) - { - mainWindow.Show(); - } + mainWindow.Show(); + } - MainWindow = mainWindow; - }); - } + MainWindow = mainWindow; + } return Run(new CancellationTokenSource()); } @@ -362,7 +359,7 @@ namespace Avalonia } /// - bool IResourceProvider.TryGetResource(string key, out object value) + bool IResourceProvider.TryGetResource(object key, out object value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index cc9e6b7444..b09d3bddff 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -33,8 +33,6 @@ namespace Avalonia.Controls /// public class Button : ContentControl { - private ICommand _command; - /// /// Defines the property. /// @@ -75,6 +73,9 @@ namespace Avalonia.Controls public static readonly StyledProperty IsPressedProperty = AvaloniaProperty.Register(nameof(IsPressed)); + private ICommand _command; + private bool _commandCanExecute = true; + /// /// Initializes static members of the class. /// @@ -147,6 +148,8 @@ namespace Avalonia.Controls private set { SetValue(IsPressedProperty, value); } } + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -252,7 +255,6 @@ namespace Avalonia.Controls if (e.MouseButton == MouseButton.Left) { - e.Device.Capture(this); IsPressed = true; e.Handled = true; @@ -270,7 +272,6 @@ namespace Avalonia.Controls if (IsPressed && e.MouseButton == MouseButton.Left) { - e.Device.Capture(null); IsPressed = false; e.Handled = true; @@ -282,6 +283,11 @@ namespace Avalonia.Controls } } + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + IsPressed = false; + } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) { base.UpdateDataValidation(property, status); @@ -289,7 +295,11 @@ namespace Avalonia.Controls { if (status?.ErrorType == BindingErrorType.Error) { - IsEnabled = false; + if (_commandCanExecute) + { + _commandCanExecute = false; + UpdateIsEffectivelyEnabled(); + } } } } @@ -348,9 +358,13 @@ namespace Avalonia.Controls /// The event args. private void CanExecuteChanged(object sender, EventArgs e) { - // HACK: Just set the IsEnabled property for the moment. This needs to be changed to - // use IsEnabledCore etc. but it will do for now. - IsEnabled = Command == null || Command.CanExecute(CommandParameter); + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } } /// diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index 224d9b782b..53852defb3 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -176,18 +176,5 @@ namespace Avalonia.Controls.Primitives if (e.MouseButton == MouseButton.Left) CalendarLeftMouseButtonUp?.Invoke(this, e); } - - /// - /// We need to simulate the MouseLeftButtonUp event for the - /// CalendarButton that stays in Pressed state after MouseCapture is - /// released since there is no actual MouseLeftButtonUp event for the - /// release. - /// - /// Event arguments. - internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e) - { - e.Handled = false; - base.OnPointerReleased(e); - } } } diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index 1b36f92fd3..cb2a98e5ca 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -234,18 +234,5 @@ namespace Avalonia.Controls.Primitives if (e.MouseButton == MouseButton.Left) CalendarDayButtonMouseUp?.Invoke(this, e); } - - /// - /// We need to simulate the MouseLeftButtonUp event for the - /// CalendarDayButton that stays in Pressed state after MouseCapture is - /// released since there is no actual MouseLeftButtonUp event for the - /// release. - /// - /// Event arguments. - internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e) - { - e.Handled = false; - base.OnPointerReleased(e); - } } } diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index fb6dacaf81..8232697c18 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -934,22 +934,6 @@ namespace Avalonia.Controls.Primitives // The button is in Pressed state. Change the state to normal. if (e.Device.Captured == b) e.Device.Capture(null); - // null check is added for unit tests - if (_downEventArg != null) - { - var arg = - new PointerReleasedEventArgs() - { - Device = _downEventArg.Device, - MouseButton = _downEventArg.MouseButton, - Handled = _downEventArg.Handled, - InputModifiers = _downEventArg.InputModifiers, - Route = _downEventArg.Route, - Source = _downEventArg.Source - }; - - b.SendMouseLeftButtonUp(arg); - } _lastCalendarDayButton = b; } } @@ -1221,21 +1205,7 @@ namespace Avalonia.Controls.Primitives if (e.Device.Captured == b) e.Device.Capture(null); //b.ReleaseMouseCapture(); - if (_downEventArgYearView != null) - { - var args = - new PointerReleasedEventArgs() - { - Device = _downEventArgYearView.Device, - MouseButton = _downEventArgYearView.MouseButton, - Handled = _downEventArgYearView.Handled, - InputModifiers = _downEventArgYearView.InputModifiers, - Route = _downEventArgYearView.Route, - Source = _downEventArgYearView.Source - }; - - b.SendMouseLeftButtonUp(args); - } + _lastCalendarButton = b; } } diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index d316881a05..e3d2489241 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -55,11 +55,7 @@ namespace Avalonia.Controls /// /// Gets the actual calculated width of the column. /// - public double ActualWidth - { - get; - internal set; - } + public double ActualWidth => Parent?.GetFinalColumnDefinitionWidth(Index) ?? 0d; /// /// Gets or sets the maximum width of the column in DIPs. @@ -87,5 +83,9 @@ namespace Avalonia.Controls get { return GetValue(WidthProperty); } set { SetValue(WidthProperty, value); } } + + internal override GridLength UserSizeValueCache => this.Width; + internal override double UserMinSizeValueCache => this.MinWidth; + internal override double UserMaxSizeValueCache => this.MaxWidth; } } diff --git a/src/Avalonia.Controls/ColumnDefinitions.cs b/src/Avalonia.Controls/ColumnDefinitions.cs index ecfe6027ac..d4fc2ee0a1 100644 --- a/src/Avalonia.Controls/ColumnDefinitions.cs +++ b/src/Avalonia.Controls/ColumnDefinitions.cs @@ -1,6 +1,8 @@ // 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 System.Collections.Specialized; using System.Linq; using Avalonia.Collections; @@ -9,14 +11,13 @@ namespace Avalonia.Controls /// /// A collection of s. /// - public class ColumnDefinitions : AvaloniaList + public class ColumnDefinitions : DefinitionList { /// /// Initializes a new instance of the class. /// - public ColumnDefinitions() + public ColumnDefinitions() : base () { - ResetBehavior = ResetBehavior.Remove; } /// diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index bf79e192c5..5d427df5a6 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -302,7 +302,7 @@ namespace Avalonia.Controls } } - private bool CanFocus(IControl control) => control.Focusable && control.IsEnabledCore && control.IsVisible; + private bool CanFocus(IControl control) => control.Focusable && control.IsEffectivelyEnabled && control.IsVisible; private void UpdateSelectionBoxItem(object item) { diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 5726356830..a68fe1265f 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -1,26 +1,734 @@ -// 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. +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +using Avalonia; +using Avalonia.Collections; +using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// Base class for and . + /// DefinitionBase provides core functionality used internally by Grid + /// and ColumnDefinitionCollection / RowDefinitionCollection /// - public class DefinitionBase : AvaloniaObject + public abstract class DefinitionBase : AvaloniaObject { /// - /// Defines the property. + /// SharedSizeGroup property. /// - public static readonly StyledProperty SharedSizeGroupProperty = - AvaloniaProperty.Register(nameof(SharedSizeGroup), inherits: true); + public string SharedSizeGroup + { + get { return (string)GetValue(SharedSizeGroupProperty); } + set { SetValue(SharedSizeGroupProperty, value); } + } /// - /// Gets or sets the name of the shared size group of the column or row. + /// Callback to notify about entering model tree. /// - public string SharedSizeGroup + internal void OnEnterParentTree() { - get { return GetValue(SharedSizeGroupProperty); } - set { SetValue(SharedSizeGroupProperty, value); } + this.InheritanceParent = Parent; + if (_sharedState == null) + { + // start with getting SharedSizeGroup value. + // this property is NOT inhereted which should result in better overall perf. + string sharedSizeGroupId = SharedSizeGroup; + if (sharedSizeGroupId != null) + { + SharedSizeScope privateSharedSizeScope = PrivateSharedSizeScope; + if (privateSharedSizeScope != null) + { + _sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); + _sharedState.AddMember(this); + } + } + } + } + + /// + /// Callback to notify about exitting model tree. + /// + internal void OnExitParentTree() + { + _offset = 0; + if (_sharedState != null) + { + _sharedState.RemoveMember(this); + _sharedState = null; + } + } + + /// + /// Performs action preparing definition to enter layout calculation mode. + /// + internal void OnBeforeLayout(Grid grid) + { + // reset layout state. + _minSize = 0; + LayoutWasUpdated = true; + + // defer verification for shared definitions + if (_sharedState != null) { _sharedState.EnsureDeferredValidation(grid); } + } + + /// + /// Updates min size. + /// + /// New size. + internal void UpdateMinSize(double minSize) + { + _minSize = Math.Max(_minSize, minSize); + } + + /// + /// Sets min size. + /// + /// New size. + internal void SetMinSize(double minSize) + { + _minSize = minSize; + } + + /// + /// This method reflects Grid.SharedScopeProperty state by setting / clearing + /// dynamic property PrivateSharedSizeScopeProperty. Value of PrivateSharedSizeScopeProperty + /// is a collection of SharedSizeState objects for the scope. + /// + internal static void OnIsSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + if ((bool)e.NewValue) + { + SharedSizeScope sharedStatesCollection = new SharedSizeScope(); + d.SetValue(PrivateSharedSizeScopeProperty, sharedStatesCollection); + } + else + { + d.ClearValue(PrivateSharedSizeScopeProperty); + } + } + + /// + /// Returns true if this definition is a part of shared group. + /// + internal bool IsShared + { + get { return (_sharedState != null); } + } + + /// + /// Internal accessor to user size field. + /// + internal GridLength UserSize + { + get { return (_sharedState != null ? _sharedState.UserSize : UserSizeValueCache); } + } + + /// + /// Internal accessor to user min size field. + /// + internal double UserMinSize + { + get { return (UserMinSizeValueCache); } + } + + /// + /// Internal accessor to user max size field. + /// + internal double UserMaxSize + { + get { return (UserMaxSizeValueCache); } + } + + /// + /// DefinitionBase's index in the parents collection. + /// + internal int Index + { + get + { + return (_parentIndex); + } + set + { + Debug.Assert(value >= -1); + _parentIndex = value; + } + } + + /// + /// Layout-time user size type. + /// + internal Grid.LayoutTimeSizeType SizeType + { + get { return (_sizeType); } + set { _sizeType = value; } + } + + /// + /// Returns or sets measure size for the definition. + /// + internal double MeasureSize + { + get { return (_measureSize); } + set { _measureSize = value; } + } + + /// + /// Returns definition's layout time type sensitive preferred size. + /// + /// + /// Returned value is guaranteed to be true preferred size. + /// + internal double PreferredSize + { + get + { + double preferredSize = MinSize; + if (_sizeType != Grid.LayoutTimeSizeType.Auto + && preferredSize < _measureSize) + { + preferredSize = _measureSize; + } + return (preferredSize); + } + } + + /// + /// Returns or sets size cache for the definition. + /// + internal double SizeCache + { + get { return (_sizeCache); } + set { _sizeCache = value; } + } + + /// + /// Returns min size. + /// + internal double MinSize + { + get + { + double minSize = _minSize; + if (UseSharedMinimum + && _sharedState != null + && minSize < _sharedState.MinSize) + { + minSize = _sharedState.MinSize; + } + return (minSize); + } + } + + /// + /// Returns min size, always taking into account shared state. + /// + internal double MinSizeForArrange + { + get + { + double minSize = _minSize; + if (_sharedState != null + && (UseSharedMinimum || !LayoutWasUpdated) + && minSize < _sharedState.MinSize) + { + minSize = _sharedState.MinSize; + } + return (minSize); + } + } + + /// + /// Offset. + /// + internal double FinalOffset + { + get { return _offset; } + set { _offset = value; } + } + + /// + /// Internal helper to access up-to-date UserSize property value. + /// + internal abstract GridLength UserSizeValueCache { get; } + + /// + /// Internal helper to access up-to-date UserMinSize property value. + /// + internal abstract double UserMinSizeValueCache { get; } + + /// + /// Internal helper to access up-to-date UserMaxSize property value. + /// + internal abstract double UserMaxSizeValueCache { get; } + + internal Grid Parent { get; set; } + + /// + /// SetFlags is used to set or unset one or multiple + /// flags on the object. + /// + private void SetFlags(bool value, Flags flags) + { + _flags = value ? (_flags | flags) : (_flags & (~flags)); + } + + /// + /// CheckFlagsAnd returns true if all the flags in the + /// given bitmask are set on the object. + /// + private bool CheckFlagsAnd(Flags flags) + { + return ((_flags & flags) == flags); + } + + private static void OnSharedSizeGroupPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + DefinitionBase definition = (DefinitionBase)d; + + if (definition.Parent != null) + { + string sharedSizeGroupId = (string)e.NewValue; + + if (definition._sharedState != null) + { + // if definition is already registered AND shared size group id is changing, + // then un-register the definition from the current shared size state object. + definition._sharedState.RemoveMember(definition); + definition._sharedState = null; + } + + if ((definition._sharedState == null) && (sharedSizeGroupId != null)) + { + SharedSizeScope privateSharedSizeScope = definition.PrivateSharedSizeScope; + if (privateSharedSizeScope != null) + { + // if definition is not registered and both: shared size group id AND private shared scope + // are available, then register definition. + definition._sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); + definition._sharedState.AddMember(definition); + } + } + } + } + + /// + /// Verifies that Shared Size Group Property string + /// a) not empty. + /// b) contains only letters, digits and underscore ('_'). + /// c) does not start with a digit. + /// + private static string SharedSizeGroupPropertyValueValid(Control _, string value) + { + Contract.Requires(value != null); + + string id = (string)value; + + if (id != string.Empty) + { + int i = -1; + while (++i < id.Length) + { + bool isDigit = Char.IsDigit(id[i]); + + if ((i == 0 && isDigit) + || !(isDigit + || Char.IsLetter(id[i]) + || '_' == id[i])) + { + break; + } + } + + if (i == id.Length) + { + return value; + } + } + + throw new ArgumentException("Invalid SharedSizeGroup string."); + } + + /// + /// OnPrivateSharedSizeScopePropertyChanged is called when new scope enters or + /// existing scope just left. In both cases if the DefinitionBase object is already registered + /// in SharedSizeState, it should un-register and register itself in a new one. + /// + private static void OnPrivateSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + DefinitionBase definition = (DefinitionBase)d; + + if (definition.Parent != null) + { + SharedSizeScope privateSharedSizeScope = (SharedSizeScope)e.NewValue; + + if (definition._sharedState != null) + { + // if definition is already registered And shared size scope is changing, + // then un-register the definition from the current shared size state object. + definition._sharedState.RemoveMember(definition); + definition._sharedState = null; + } + + if ((definition._sharedState == null) && (privateSharedSizeScope != null)) + { + string sharedSizeGroup = definition.SharedSizeGroup; + if (sharedSizeGroup != null) + { + // if definition is not registered and both: shared size group id AND private shared scope + // are available, then register definition. + definition._sharedState = privateSharedSizeScope.EnsureSharedState(definition.SharedSizeGroup); + definition._sharedState.AddMember(definition); + } + } + } + } + + /// + /// Private getter of shared state collection dynamic property. + /// + private SharedSizeScope PrivateSharedSizeScope + { + get { return (SharedSizeScope)GetValue(PrivateSharedSizeScopeProperty); } + } + + /// + /// Convenience accessor to UseSharedMinimum flag + /// + private bool UseSharedMinimum + { + get { return (CheckFlagsAnd(Flags.UseSharedMinimum)); } + set { SetFlags(value, Flags.UseSharedMinimum); } + } + + /// + /// Convenience accessor to LayoutWasUpdated flag + /// + private bool LayoutWasUpdated + { + get { return (CheckFlagsAnd(Flags.LayoutWasUpdated)); } + set { SetFlags(value, Flags.LayoutWasUpdated); } + } + + private Flags _flags; // flags reflecting various aspects of internal state + internal int _parentIndex = -1; // this instance's index in parent's children collection + + private Grid.LayoutTimeSizeType _sizeType; // layout-time user size type. it may differ from _userSizeValueCache.UnitType when calculating "to-content" + + private double _minSize; // used during measure to accumulate size for "Auto" and "Star" DefinitionBase's + private double _measureSize; // size, calculated to be the input contstraint size for Child.Measure + private double _sizeCache; // cache used for various purposes (sorting, caching, etc) during calculations + private double _offset; // offset of the DefinitionBase from left / top corner (assuming LTR case) + + private SharedSizeState _sharedState; // reference to shared state object this instance is registered with + + [System.Flags] + private enum Flags : byte + { + // + // bool flags + // + UseSharedMinimum = 0x00000020, // when "1", definition will take into account shared state's minimum + LayoutWasUpdated = 0x00000040, // set to "1" every time the parent grid is measured + } + + /// + /// Collection of shared states objects for a single scope + /// + internal class SharedSizeScope + { + /// + /// Returns SharedSizeState object for a given group. + /// Creates a new StatedState object if necessary. + /// + internal SharedSizeState EnsureSharedState(string sharedSizeGroup) + { + // check that sharedSizeGroup is not default + Debug.Assert(sharedSizeGroup != null); + + SharedSizeState sharedState = _registry[sharedSizeGroup] as SharedSizeState; + if (sharedState == null) + { + sharedState = new SharedSizeState(this, sharedSizeGroup); + _registry[sharedSizeGroup] = sharedState; + } + return (sharedState); + } + + /// + /// Removes an entry in the registry by the given key. + /// + internal void Remove(object key) + { + Debug.Assert(_registry.Contains(key)); + _registry.Remove(key); + } + + private Hashtable _registry = new Hashtable(); // storage for shared state objects + } + + /// + /// Implementation of per shared group state object + /// + internal class SharedSizeState + { + /// + /// Default ctor. + /// + internal SharedSizeState(SharedSizeScope sharedSizeScope, string sharedSizeGroupId) + { + Debug.Assert(sharedSizeScope != null && sharedSizeGroupId != null); + _sharedSizeScope = sharedSizeScope; + _sharedSizeGroupId = sharedSizeGroupId; + _registry = new List(); + _layoutUpdated = new EventHandler(OnLayoutUpdated); + _broadcastInvalidation = true; + } + + /// + /// Adds / registers a definition instance. + /// + internal void AddMember(DefinitionBase member) + { + Debug.Assert(!_registry.Contains(member)); + _registry.Add(member); + Invalidate(); + } + + /// + /// Removes / un-registers a definition instance. + /// + /// + /// If the collection of registered definitions becomes empty + /// instantiates self removal from owner's collection. + /// + internal void RemoveMember(DefinitionBase member) + { + Invalidate(); + _registry.Remove(member); + + if (_registry.Count == 0) + { + _sharedSizeScope.Remove(_sharedSizeGroupId); + } + } + + /// + /// Propogates invalidations for all registered definitions. + /// Resets its own state. + /// + internal void Invalidate() + { + _userSizeValid = false; + + if (_broadcastInvalidation) + { + for (int i = 0, count = _registry.Count; i < count; ++i) + { + Grid parentGrid = (Grid)(_registry[i].Parent); + parentGrid.Invalidate(); + } + _broadcastInvalidation = false; + } + } + + /// + /// Makes sure that one and only one layout updated handler is registered for this shared state. + /// + internal void EnsureDeferredValidation(Control layoutUpdatedHost) + { + if (_layoutUpdatedHost == null) + { + _layoutUpdatedHost = layoutUpdatedHost; + _layoutUpdatedHost.LayoutUpdated += _layoutUpdated; + } + } + + /// + /// DefinitionBase's specific code. + /// + internal double MinSize + { + get + { + if (!_userSizeValid) { EnsureUserSizeValid(); } + return (_minSize); + } + } + + /// + /// DefinitionBase's specific code. + /// + internal GridLength UserSize + { + get + { + if (!_userSizeValid) { EnsureUserSizeValid(); } + return (_userSize); + } + } + + private void EnsureUserSizeValid() + { + _userSize = new GridLength(1, GridUnitType.Auto); + + for (int i = 0, count = _registry.Count; i < count; ++i) + { + Debug.Assert(_userSize.GridUnitType == GridUnitType.Auto + || _userSize.GridUnitType == GridUnitType.Pixel); + + GridLength currentGridLength = _registry[i].UserSizeValueCache; + if (currentGridLength.GridUnitType == GridUnitType.Pixel) + { + if (_userSize.GridUnitType == GridUnitType.Auto) + { + _userSize = currentGridLength; + } + else if (_userSize.Value < currentGridLength.Value) + { + _userSize = currentGridLength; + } + } + } + // taking maximum with user size effectively prevents squishy-ness. + // this is a "solution" to avoid shared definitions from been sized to + // different final size at arrange time, if / when different grids receive + // different final sizes. + _minSize = _userSize.IsAbsolute ? _userSize.Value : 0.0; + + _userSizeValid = true; + } + + /// + /// OnLayoutUpdated handler. Validates that all participating definitions + /// have updated min size value. Forces another layout update cycle if needed. + /// + private void OnLayoutUpdated(object sender, EventArgs e) + { + double sharedMinSize = 0; + + // accumulate min size of all participating definitions + for (int i = 0, count = _registry.Count; i < count; ++i) + { + sharedMinSize = Math.Max(sharedMinSize, _registry[i].MinSize); + } + + bool sharedMinSizeChanged = !MathUtilities.AreClose(_minSize, sharedMinSize); + + // compare accumulated min size with min sizes of the individual definitions + for (int i = 0, count = _registry.Count; i < count; ++i) + { + DefinitionBase definitionBase = _registry[i]; + + if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated) + { + // if definition's min size is different, then need to re-measure + if (!MathUtilities.AreClose(sharedMinSize, definitionBase.MinSize)) + { + Grid parentGrid = (Grid)definitionBase.Parent; + parentGrid.InvalidateMeasure(); + definitionBase.UseSharedMinimum = true; + } + else + { + definitionBase.UseSharedMinimum = false; + + // if measure is valid then also need to check arrange. + // Note: definitionBase.SizeCache is volatile but at this point + // it contains up-to-date final size + if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache)) + { + Grid parentGrid = (Grid)definitionBase.Parent; + parentGrid.InvalidateArrange(); + } + } + + definitionBase.LayoutWasUpdated = false; + } + } + + _minSize = sharedMinSize; + + _layoutUpdatedHost.LayoutUpdated -= _layoutUpdated; + _layoutUpdatedHost = null; + + _broadcastInvalidation = true; + } + + // the scope this state belongs to + private readonly SharedSizeScope _sharedSizeScope; + + // Id of the shared size group this object is servicing + private readonly string _sharedSizeGroupId; + + // Registry of participating definitions + private readonly List _registry; + + // Instance event handler for layout updated event + private readonly EventHandler _layoutUpdated; + + // Control for which layout updated event handler is registered + private Control _layoutUpdatedHost; + + // "true" when broadcasting of invalidation is needed + private bool _broadcastInvalidation; + + // "true" when _userSize is up to date + private bool _userSizeValid; + + // shared state + private GridLength _userSize; + + // shared state + private double _minSize; + } + + /// + /// Private shared size scope property holds a collection of shared state objects for the a given shared size scope. + /// + /// + internal static readonly AttachedProperty PrivateSharedSizeScopeProperty = + AvaloniaProperty.RegisterAttached( + "PrivateSharedSizeScope", + defaultValue: null, + inherits: true); + + /// + /// Shared size group property marks column / row definition as belonging to a group "Foo" or "Bar". + /// + /// + /// Value of the Shared Size Group Property must satisfy the following rules: + /// + /// + /// String must not be empty. + /// + /// + /// String must consist of letters, digits and underscore ('_') only. + /// + /// + /// String must not start with a digit. + /// + /// + /// + public static readonly AttachedProperty SharedSizeGroupProperty = + AvaloniaProperty.RegisterAttached( + "SharedSizeGroup", + validate: SharedSizeGroupPropertyValueValid); + + /// + /// Static ctor. Used for static registration of properties. + /// + static DefinitionBase() + { + SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); + PrivateSharedSizeScopeProperty.Changed.AddClassHandler(OnPrivateSharedSizeScopePropertyChanged); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/DefinitionList.cs b/src/Avalonia.Controls/DefinitionList.cs new file mode 100644 index 0000000000..97d92bdcb7 --- /dev/null +++ b/src/Avalonia.Controls/DefinitionList.cs @@ -0,0 +1,60 @@ +// 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 System.Collections.Specialized; +using System.Linq; +using Avalonia.Collections; + +namespace Avalonia.Controls +{ + public abstract class DefinitionList : AvaloniaList where T : DefinitionBase + { + public DefinitionList() + { + ResetBehavior = ResetBehavior.Remove; + CollectionChanged += OnCollectionChanged; + } + + internal bool IsDirty = true; + private Grid _parent; + + internal Grid Parent + { + get => _parent; + set => SetParent(value); + } + + private void SetParent(Grid value) + { + _parent = value; + + foreach (var pair in this.Select((definitions, index) => (definitions, index))) + { + pair.definitions.Parent = value; + pair.definitions.Index = pair.index; + } + } + + internal void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + foreach (var nI in this.Select((d, i) => (d, i))) + nI.d._parentIndex = nI.i; + + foreach (var nD in e.NewItems?.Cast() + ?? Enumerable.Empty()) + { + nD.Parent = this.Parent; + nD.OnEnterParentTree(); + } + + foreach (var oD in e.OldItems?.Cast() + ?? Enumerable.Empty()) + { + oD.OnExitParentTree(); + } + + IsDirty = true; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 90a27d0b31..4f41b0bf1e 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -1,601 +1,3373 @@ -// 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. +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Reactive.Linq; -using System.Runtime.CompilerServices; +using System.Threading; +using Avalonia; using Avalonia.Collections; -using Avalonia.Controls.Utils; +using Avalonia.Media; +using Avalonia.Utilities; using Avalonia.VisualTree; -using JetBrains.Annotations; namespace Avalonia.Controls { /// - /// Lays out child controls according to a grid. + /// Grid /// public class Grid : Panel { + static Grid() + { + IsSharedSizeScopeProperty.Changed.AddClassHandler(DefinitionBase.OnIsSharedSizeScopePropertyChanged); + ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); + + ColumnProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + ColumnSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + RowProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + RowSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); + } + + /// + /// Default constructor. + /// + public Grid() + { + } + + /// + /// Helper for setting Column property on a Control. + /// + /// Control to set Column property on. + /// Column property value. + public static void SetColumn(Control element, int value) + { + Contract.Requires(element != null); + element.SetValue(ColumnProperty, value); + } + + /// + /// Helper for reading Column property from a Control. + /// + /// Control to read Column property from. + /// Column property value. + public static int GetColumn(Control element) + { + Contract.Requires(element != null); + return element.GetValue(ColumnProperty); + } + + /// + /// Helper for setting Row property on a Control. + /// + /// Control to set Row property on. + /// Row property value. + public static void SetRow(Control element, int value) + { + Contract.Requires(element != null); + element.SetValue(RowProperty, value); + } + + /// + /// Helper for reading Row property from a Control. + /// + /// Control to read Row property from. + /// Row property value. + public static int GetRow(Control element) + { + Contract.Requires(element != null); + return element.GetValue(RowProperty); + } + + /// + /// Helper for setting ColumnSpan property on a Control. + /// + /// Control to set ColumnSpan property on. + /// ColumnSpan property value. + public static void SetColumnSpan(Control element, int value) + { + Contract.Requires(element != null); + element.SetValue(ColumnSpanProperty, value); + } + + /// + /// Helper for reading ColumnSpan property from a Control. + /// + /// Control to read ColumnSpan property from. + /// ColumnSpan property value. + public static int GetColumnSpan(Control element) + { + Contract.Requires(element != null); + return element.GetValue(ColumnSpanProperty); + } + + /// + /// Helper for setting RowSpan property on a Control. + /// + /// Control to set RowSpan property on. + /// RowSpan property value. + public static void SetRowSpan(Control element, int value) + { + Contract.Requires(element != null); + element.SetValue(RowSpanProperty, value); + } + + /// + /// Helper for reading RowSpan property from a Control. + /// + /// Control to read RowSpan property from. + /// RowSpan property value. + public static int GetRowSpan(Control element) + { + Contract.Requires(element != null); + return element.GetValue(RowSpanProperty); + } + + /// + /// Helper for setting IsSharedSizeScope property on a Control. + /// + /// Control to set IsSharedSizeScope property on. + /// IsSharedSizeScope property value. + public static void SetIsSharedSizeScope(Control element, bool value) + { + Contract.Requires(element != null); + element.SetValue(IsSharedSizeScopeProperty, value); + } + + /// + /// Helper for reading IsSharedSizeScope property from a Control. + /// + /// Control to read IsSharedSizeScope property from. + /// IsSharedSizeScope property value. + public static bool GetIsSharedSizeScope(Control element) + { + Contract.Requires(element != null); + return element.GetValue(IsSharedSizeScopeProperty); + } + + /// + /// ShowGridLines property. + /// + public bool ShowGridLines + { + get { return GetValue(ShowGridLinesProperty); } + set { SetValue(ShowGridLinesProperty, value); } + } + + /// + /// Returns a ColumnDefinitions of column definitions. + /// + public ColumnDefinitions ColumnDefinitions + { + get + { + if (_data == null) { _data = new ExtendedData(); } + if (_data.ColumnDefinitions == null) { _data.ColumnDefinitions = new ColumnDefinitions() { Parent = this }; } + + return (_data.ColumnDefinitions); + } + set + { + if (_data == null) { _data = new ExtendedData(); } + _data.ColumnDefinitions = value; + _data.ColumnDefinitions.Parent = this; + } + } + + /// + /// Returns a RowDefinitions of row definitions. + /// + public RowDefinitions RowDefinitions + { + get + { + if (_data == null) { _data = new ExtendedData(); } + if (_data.RowDefinitions == null) { _data.RowDefinitions = new RowDefinitions() { Parent = this }; } + + return (_data.RowDefinitions); + } + set + { + if (_data == null) { _data = new ExtendedData(); } + _data.RowDefinitions = value; + _data.RowDefinitions.Parent = this; + } + } + + /// + /// Content measurement. + /// + /// Constraint + /// Desired size + protected override Size MeasureOverride(Size constraint) + { + Size gridDesiredSize; + ExtendedData extData = ExtData; + + try + { + ListenToNotifications = true; + MeasureOverrideInProgress = true; + + if (extData == null) + { + gridDesiredSize = new Size(); + var children = this.Children; + + for (int i = 0, count = children.Count; i < count; ++i) + { + var child = children[i]; + if (child != null) + { + child.Measure(constraint); + gridDesiredSize = new Size(Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), + Math.Max(gridDesiredSize.Height, child.DesiredSize.Height)); + } + } + } + else + { + { + bool sizeToContentU = double.IsPositiveInfinity(constraint.Width); + bool sizeToContentV = double.IsPositiveInfinity(constraint.Height); + + // Clear index information and rounding errors + if (RowDefinitionsDirty || ColumnDefinitionsDirty) + { + if (_definitionIndices != null) + { + Array.Clear(_definitionIndices, 0, _definitionIndices.Length); + _definitionIndices = null; + } + + if (UseLayoutRounding) + { + if (_roundingErrors != null) + { + Array.Clear(_roundingErrors, 0, _roundingErrors.Length); + _roundingErrors = null; + } + } + } + + ValidateDefinitionsUStructure(); + ValidateDefinitionsLayout(DefinitionsU, sizeToContentU); + + ValidateDefinitionsVStructure(); + ValidateDefinitionsLayout(DefinitionsV, sizeToContentV); + + CellsStructureDirty |= (SizeToContentU != sizeToContentU) || (SizeToContentV != sizeToContentV); + + SizeToContentU = sizeToContentU; + SizeToContentV = sizeToContentV; + } + + ValidateCells(); + + Debug.Assert(DefinitionsU.Count > 0 && DefinitionsV.Count > 0); + + // Grid classifies cells into four groups depending on + // the column / row type a cell belongs to (number corresponds to + // group number): + // + // Px Auto Star + // +--------+--------+--------+ + // | | | | + // Px | 1 | 1 | 3 | + // | | | | + // +--------+--------+--------+ + // | | | | + // Auto | 1 | 1 | 3 | + // | | | | + // +--------+--------+--------+ + // | | | | + // Star | 4 | 2 | 4 | + // | | | | + // +--------+--------+--------+ + // + // The group number indicates the order in which cells are measured. + // Certain order is necessary to be able to dynamically resolve star + // columns / rows sizes which are used as input for measuring of + // the cells belonging to them. + // + // However, there are cases when topology of a grid causes cyclical + // size dependences. For example: + // + // + // column width="Auto" column width="*" + // +----------------------+----------------------+ + // | | | + // | | | + // | | | + // | | | + // row height="Auto" | | cell 1 2 | + // | | | + // | | | + // | | | + // | | | + // +----------------------+----------------------+ + // | | | + // | | | + // | | | + // | | | + // row height="*" | cell 2 1 | | + // | | | + // | | | + // | | | + // | | | + // +----------------------+----------------------+ + // + // In order to accurately calculate constraint width for "cell 1 2" + // (which is the remaining of grid's available width and calculated + // value of Auto column), "cell 2 1" needs to be calculated first, + // as it contributes to the Auto column's calculated value. + // At the same time in order to accurately calculate constraint + // height for "cell 2 1", "cell 1 2" needs to be calcualted first, + // as it contributes to Auto row height, which is used in the + // computation of Star row resolved height. + // + // to "break" this cyclical dependency we are making (arbitrary) + // decision to treat cells like "cell 2 1" as if they appear in Auto + // rows. And then recalculate them one more time when star row + // heights are resolved. + // + // (Or more strictly) the code below implement the following logic: + // + // +---------+ + // | enter | + // +---------+ + // | + // V + // +----------------+ + // | Measure Group1 | + // +----------------+ + // | + // V + // / - \ + // / \ + // Y / Can \ N + // +--------| Resolve |-----------+ + // | \ StarsV? / | + // | \ / | + // | \ - / | + // V V + // +----------------+ / - \ + // | Resolve StarsV | / \ + // +----------------+ Y / Can \ N + // | +----| Resolve |------+ + // V | \ StarsU? / | + // +----------------+ | \ / | + // | Measure Group2 | | \ - / | + // +----------------+ | V + // | | +-----------------+ + // V | | Measure Group2' | + // +----------------+ | +-----------------+ + // | Resolve StarsU | | | + // +----------------+ V V + // | +----------------+ +----------------+ + // V | Resolve StarsU | | Resolve StarsU | + // +----------------+ +----------------+ +----------------+ + // | Measure Group3 | | | + // +----------------+ V V + // | +----------------+ +----------------+ + // | | Measure Group3 | | Measure Group3 | + // | +----------------+ +----------------+ + // | | | + // | V V + // | +----------------+ +----------------+ + // | | Resolve StarsV | | Resolve StarsV | + // | +----------------+ +----------------+ + // | | | + // | | V + // | | +------------------+ + // | | | Measure Group2'' | + // | | +------------------+ + // | | | + // +----------------------+-------------------------+ + // | + // V + // +----------------+ + // | Measure Group4 | + // +----------------+ + // | + // V + // +--------+ + // | exit | + // +--------+ + // + // where: + // * all [Measure GroupN] - regular children measure process - + // each cell is measured given contraint size as an input + // and each cell's desired size is accumulated on the + // corresponding column / row; + // * [Measure Group2'] - is when each cell is measured with + // infinit height as a constraint and a cell's desired + // height is ignored; + // * [Measure Groups''] - is when each cell is measured (second + // time during single Grid.MeasureOverride) regularly but its + // returned width is ignored; + // + // This algorithm is believed to be as close to ideal as possible. + // It has the following drawbacks: + // * cells belonging to Group2 can be called to measure twice; + // * iff during second measure a cell belonging to Group2 returns + // desired width greater than desired width returned the first + // time, such a cell is going to be clipped, even though it + // appears in Auto column. + // + + MeasureCellsGroup(extData.CellGroup1, constraint, false, false); + + { + // after Group1 is measured, only Group3 may have cells belonging to Auto rows. + bool canResolveStarsV = !HasGroup3CellsInAutoRows; + + if (canResolveStarsV) + { + if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } + MeasureCellsGroup(extData.CellGroup2, constraint, false, false); + if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } + MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + } + else + { + // if at least one cell exists in Group2, it must be measured before + // StarsU can be resolved. + bool canResolveStarsU = extData.CellGroup2 > PrivateCells.Length; + if (canResolveStarsU) + { + if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } + MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } + } + else + { + // This is a revision to the algorithm employed for the cyclic + // dependency case described above. We now repeatedly + // measure Group3 and Group2 until their sizes settle. We + // also use a count heuristic to break a loop in case of one. + + bool hasDesiredSizeUChanged = false; + int cnt = 0; + + // Cache Group2MinWidths & Group3MinHeights + double[] group2MinSizes = CacheMinSizes(extData.CellGroup2, false); + double[] group3MinSizes = CacheMinSizes(extData.CellGroup3, true); + + MeasureCellsGroup(extData.CellGroup2, constraint, false, true); + + do + { + if (hasDesiredSizeUChanged) + { + // Reset cached Group3Heights + ApplyCachedMinSizes(group3MinSizes, true); + } + + if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } + MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + + // Reset cached Group2Widths + ApplyCachedMinSizes(group2MinSizes, false); + + if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } + MeasureCellsGroup(extData.CellGroup2, constraint, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged); + } + while (hasDesiredSizeUChanged && ++cnt <= c_layoutLoopMaxCount); + } + } + } + + MeasureCellsGroup(extData.CellGroup4, constraint, false, false); + + gridDesiredSize = new Size( + CalculateDesiredSize(DefinitionsU), + CalculateDesiredSize(DefinitionsV)); + } + } + finally + { + MeasureOverrideInProgress = false; + } + + return (gridDesiredSize); + } + + /// + /// Content arrangement. + /// + /// Arrange size + protected override Size ArrangeOverride(Size arrangeSize) + { + try + { + ArrangeOverrideInProgress = true; + + if (_data == null) + { + var children = this.Children; + + for (int i = 0, count = children.Count; i < count; ++i) + { + var child = children[i]; + if (child != null) + { + child.Arrange(new Rect(arrangeSize)); + } + } + } + else + { + Debug.Assert(DefinitionsU.Count > 0 && DefinitionsV.Count > 0); + + SetFinalSize(DefinitionsU, arrangeSize.Width, true); + SetFinalSize(DefinitionsV, arrangeSize.Height, false); + + var children = this.Children; + + for (int currentCell = 0; currentCell < PrivateCells.Length; ++currentCell) + { + var cell = children[currentCell]; + if (cell == null) + { + continue; + } + + int columnIndex = PrivateCells[currentCell].ColumnIndex; + int rowIndex = PrivateCells[currentCell].RowIndex; + int columnSpan = PrivateCells[currentCell].ColumnSpan; + int rowSpan = PrivateCells[currentCell].RowSpan; + + Rect cellRect = new Rect( + columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset, + rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset, + GetFinalSizeForRange(DefinitionsU, columnIndex, columnSpan), + GetFinalSizeForRange(DefinitionsV, rowIndex, rowSpan)); + + + cell.Arrange(cellRect); + + } + + // update render bound on grid lines renderer visual + var gridLinesRenderer = EnsureGridLinesRenderer(); + gridLinesRenderer?.UpdateRenderBounds(arrangeSize); + } + } + finally + { + SetValid(); + ArrangeOverrideInProgress = false; + } + return (arrangeSize); + } + + /// + /// Invalidates grid caches and makes the grid dirty for measure. + /// + internal void Invalidate() + { + CellsStructureDirty = true; + InvalidateMeasure(); + } + + /// + /// Returns final width for a column. + /// + /// + /// Used from public ColumnDefinition ActualWidth. Calculates final width using offset data. + /// + internal double GetFinalColumnDefinitionWidth(int columnIndex) + { + double value = 0.0; + + Contract.Requires(_data != null); + + // actual value calculations require structure to be up-to-date + if (!ColumnDefinitionsDirty) + { + IReadOnlyList definitions = DefinitionsU; + value = definitions[(columnIndex + 1) % definitions.Count].FinalOffset; + if (columnIndex != 0) { value -= definitions[columnIndex].FinalOffset; } + } + return (value); + } + + /// + /// Returns final height for a row. + /// + /// + /// Used from public RowDefinition ActualHeight. Calculates final height using offset data. + /// + internal double GetFinalRowDefinitionHeight(int rowIndex) + { + double value = 0.0; + + Contract.Requires(_data != null); + + // actual value calculations require structure to be up-to-date + if (!RowDefinitionsDirty) + { + IReadOnlyList definitions = DefinitionsV; + value = definitions[(rowIndex + 1) % definitions.Count].FinalOffset; + if (rowIndex != 0) { value -= definitions[rowIndex].FinalOffset; } + } + return (value); + } + + /// + /// Convenience accessor to MeasureOverrideInProgress bit flag. + /// + internal bool MeasureOverrideInProgress + { + get { return (CheckFlagsAnd(Flags.MeasureOverrideInProgress)); } + set { SetFlags(value, Flags.MeasureOverrideInProgress); } + } + + /// + /// Convenience accessor to ArrangeOverrideInProgress bit flag. + /// + internal bool ArrangeOverrideInProgress + { + get { return (CheckFlagsAnd(Flags.ArrangeOverrideInProgress)); } + set { SetFlags(value, Flags.ArrangeOverrideInProgress); } + } + + /// + /// Convenience accessor to ValidDefinitionsUStructure bit flag. + /// + internal bool ColumnDefinitionsDirty + { + get => ColumnDefinitions?.IsDirty ?? false; + set => ColumnDefinitions.IsDirty = value; + } + + /// + /// Convenience accessor to ValidDefinitionsVStructure bit flag. + /// + internal bool RowDefinitionsDirty + { + get => RowDefinitions?.IsDirty ?? false; + set => RowDefinitions.IsDirty = value; + } + + /// + /// Lays out cells according to rows and columns, and creates lookup grids. + /// + private void ValidateCells() + { + if (CellsStructureDirty) + { + ValidateCellsCore(); + CellsStructureDirty = false; + } + } + + /// + /// ValidateCellsCore + /// + private void ValidateCellsCore() + { + var children = this.Children; + ExtendedData extData = ExtData; + + extData.CellCachesCollection = new CellCache[children.Count]; + extData.CellGroup1 = int.MaxValue; + extData.CellGroup2 = int.MaxValue; + extData.CellGroup3 = int.MaxValue; + extData.CellGroup4 = int.MaxValue; + + bool hasStarCellsU = false; + bool hasStarCellsV = false; + bool hasGroup3CellsInAutoRows = false; + + for (int i = PrivateCells.Length - 1; i >= 0; --i) + { + var child = children[i]; + if (child == null) + { + continue; + } + + CellCache cell = new CellCache(); + + + // Read indices from the corresponding properties: + // clamp to value < number_of_columns + // column >= 0 is guaranteed by property value validation callback + cell.ColumnIndex = Math.Min(GetColumn((Control)child), DefinitionsU.Count - 1); + // clamp to value < number_of_rows + // row >= 0 is guaranteed by property value validation callback + cell.RowIndex = Math.Min(GetRow((Control)child), DefinitionsV.Count - 1); + + // Read span properties: + // clamp to not exceed beyond right side of the grid + // column_span > 0 is guaranteed by property value validation callback + cell.ColumnSpan = Math.Min(GetColumnSpan((Control)child), DefinitionsU.Count - cell.ColumnIndex); + + // clamp to not exceed beyond bottom side of the grid + // row_span > 0 is guaranteed by property value validation callback + cell.RowSpan = Math.Min(GetRowSpan((Control)child), DefinitionsV.Count - cell.RowIndex); + + Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < DefinitionsU.Count); + Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < DefinitionsV.Count); + + // Calculate and cache length types for the child. + + cell.SizeTypeU = GetLengthTypeForRange(DefinitionsU, cell.ColumnIndex, cell.ColumnSpan); + cell.SizeTypeV = GetLengthTypeForRange(DefinitionsV, cell.RowIndex, cell.RowSpan); + + hasStarCellsU |= cell.IsStarU; + hasStarCellsV |= cell.IsStarV; + + // Distribute cells into four groups. + + if (!cell.IsStarV) + { + if (!cell.IsStarU) + { + cell.Next = extData.CellGroup1; + extData.CellGroup1 = i; + } + else + { + cell.Next = extData.CellGroup3; + extData.CellGroup3 = i; + + // Remember if this cell belongs to auto row + hasGroup3CellsInAutoRows |= cell.IsAutoV; + } + } + else + { + if (cell.IsAutoU + // Note below: if spans through Star column it is NOT Auto + && !cell.IsStarU) + { + cell.Next = extData.CellGroup2; + extData.CellGroup2 = i; + } + else + { + cell.Next = extData.CellGroup4; + extData.CellGroup4 = i; + } + } + + PrivateCells[i] = cell; + } + + HasStarCellsU = hasStarCellsU; + HasStarCellsV = hasStarCellsV; + HasGroup3CellsInAutoRows = hasGroup3CellsInAutoRows; + } + + /// + /// Initializes DefinitionsU memeber either to user supplied ColumnDefinitions collection + /// or to a default single element collection. DefinitionsU gets trimmed to size. + /// + /// + /// This is one of two methods, where ColumnDefinitions and DefinitionsU are directly accessed. + /// All the rest measure / arrange / render code must use DefinitionsU. + /// + private void ValidateDefinitionsUStructure() + { + if (ColumnDefinitionsDirty) + { + ExtendedData extData = ExtData; + + if (extData.ColumnDefinitions == null) + { + if (extData.DefinitionsU == null) + { + extData.DefinitionsU = new DefinitionBase[] { new ColumnDefinition() { Parent = this } }; + } + } + else + { + if (extData.ColumnDefinitions.Count == 0) + { + // if column definitions collection is empty + // mockup array with one column + extData.DefinitionsU = new DefinitionBase[] { new ColumnDefinition() { Parent = this } }; + } + else + { + extData.DefinitionsU = extData.ColumnDefinitions; + } + } + + ColumnDefinitionsDirty = false; + } + + Debug.Assert(ExtData.DefinitionsU != null && ExtData.DefinitionsU.Count > 0); + } + + /// + /// Initializes DefinitionsV memeber either to user supplied RowDefinitions collection + /// or to a default single element collection. DefinitionsV gets trimmed to size. + /// + /// + /// This is one of two methods, where RowDefinitions and DefinitionsV are directly accessed. + /// All the rest measure / arrange / render code must use DefinitionsV. + /// + private void ValidateDefinitionsVStructure() + { + if (RowDefinitionsDirty) + { + ExtendedData extData = ExtData; + + if (extData.RowDefinitions == null) + { + if (extData.DefinitionsV == null) + { + extData.DefinitionsV = new DefinitionBase[] { new RowDefinition() { Parent = this } }; + } + } + else + { + if (extData.RowDefinitions.Count == 0) + { + // if row definitions collection is empty + // mockup array with one row + extData.DefinitionsV = new DefinitionBase[] { new RowDefinition() { Parent = this } }; + } + else + { + extData.DefinitionsV = extData.RowDefinitions; + } + } + + RowDefinitionsDirty = false; + } + + Debug.Assert(ExtData.DefinitionsV != null && ExtData.DefinitionsV.Count > 0); + } + + /// + /// Validates layout time size type information on given array of definitions. + /// Sets MinSize and MeasureSizes. + /// + /// Array of definitions to update. + /// if "true" then star definitions are treated as Auto. + private void ValidateDefinitionsLayout( + IReadOnlyList definitions, + bool treatStarAsAuto) + { + for (int i = 0; i < definitions.Count; ++i) + { + definitions[i].OnBeforeLayout(this); + + double userMinSize = definitions[i].UserMinSize; + double userMaxSize = definitions[i].UserMaxSize; + double userSize = 0; + + switch (definitions[i].UserSize.GridUnitType) + { + case (GridUnitType.Pixel): + definitions[i].SizeType = LayoutTimeSizeType.Pixel; + userSize = definitions[i].UserSize.Value; + // this was brought with NewLayout and defeats squishy behavior + userMinSize = Math.Max(userMinSize, Math.Min(userSize, userMaxSize)); + break; + case (GridUnitType.Auto): + definitions[i].SizeType = LayoutTimeSizeType.Auto; + userSize = double.PositiveInfinity; + break; + case (GridUnitType.Star): + if (treatStarAsAuto) + { + definitions[i].SizeType = LayoutTimeSizeType.Auto; + userSize = double.PositiveInfinity; + } + else + { + definitions[i].SizeType = LayoutTimeSizeType.Star; + userSize = double.PositiveInfinity; + } + break; + default: + Debug.Assert(false); + break; + } + + definitions[i].UpdateMinSize(userMinSize); + definitions[i].MeasureSize = Math.Max(userMinSize, Math.Min(userSize, userMaxSize)); + } + } + + private double[] CacheMinSizes(int cellsHead, bool isRows) + { + double[] minSizes = isRows ? new double[DefinitionsV.Count] : new double[DefinitionsU.Count]; + + for (int j = 0; j < minSizes.Length; j++) + { + minSizes[j] = -1; + } + + int i = cellsHead; + do + { + if (isRows) + { + minSizes[PrivateCells[i].RowIndex] = DefinitionsV[PrivateCells[i].RowIndex].MinSize; + } + else + { + minSizes[PrivateCells[i].ColumnIndex] = DefinitionsU[PrivateCells[i].ColumnIndex].MinSize; + } + + i = PrivateCells[i].Next; + } while (i < PrivateCells.Length); + + return minSizes; + } + + private void ApplyCachedMinSizes(double[] minSizes, bool isRows) + { + for (int i = 0; i < minSizes.Length; i++) + { + if (MathUtilities.GreaterThanOrClose(minSizes[i], 0)) + { + if (isRows) + { + DefinitionsV[i].SetMinSize(minSizes[i]); + } + else + { + DefinitionsU[i].SetMinSize(minSizes[i]); + } + } + } + } + + private void MeasureCellsGroup( + int cellsHead, + Size referenceSize, + bool ignoreDesiredSizeU, + bool forceInfinityV) + { + bool unusedHasDesiredSizeUChanged; + MeasureCellsGroup(cellsHead, referenceSize, ignoreDesiredSizeU, forceInfinityV, out unusedHasDesiredSizeUChanged); + } + + /// + /// Measures one group of cells. + /// + /// Head index of the cells chain. + /// Reference size for spanned cells + /// calculations. + /// When "true" cells' desired + /// width is not registered in columns. + /// Passed through to MeasureCell. + /// When "true" cells' desired height is not registered in rows. + private void MeasureCellsGroup( + int cellsHead, + Size referenceSize, + bool ignoreDesiredSizeU, + bool forceInfinityV, + out bool hasDesiredSizeUChanged) + { + hasDesiredSizeUChanged = false; + + if (cellsHead >= PrivateCells.Length) + { + return; + } + + var children = this.Children; + Hashtable spanStore = null; + bool ignoreDesiredSizeV = forceInfinityV; + + int i = cellsHead; + do + { + double oldWidth = children[i].DesiredSize.Width; + + MeasureCell(i, forceInfinityV); + + hasDesiredSizeUChanged |= !MathUtilities.AreClose(oldWidth, children[i].DesiredSize.Width); + + if (!ignoreDesiredSizeU) + { + if (PrivateCells[i].ColumnSpan == 1) + { + DefinitionsU[PrivateCells[i].ColumnIndex].UpdateMinSize(Math.Min(children[i].DesiredSize.Width, DefinitionsU[PrivateCells[i].ColumnIndex].UserMaxSize)); + } + else + { + RegisterSpan( + ref spanStore, + PrivateCells[i].ColumnIndex, + PrivateCells[i].ColumnSpan, + true, + children[i].DesiredSize.Width); + } + } + + if (!ignoreDesiredSizeV) + { + if (PrivateCells[i].RowSpan == 1) + { + DefinitionsV[PrivateCells[i].RowIndex].UpdateMinSize(Math.Min(children[i].DesiredSize.Height, DefinitionsV[PrivateCells[i].RowIndex].UserMaxSize)); + } + else + { + RegisterSpan( + ref spanStore, + PrivateCells[i].RowIndex, + PrivateCells[i].RowSpan, + false, + children[i].DesiredSize.Height); + } + } + + i = PrivateCells[i].Next; + } while (i < PrivateCells.Length); + + if (spanStore != null) + { + foreach (DictionaryEntry e in spanStore) + { + SpanKey key = (SpanKey)e.Key; + double requestedSize = (double)e.Value; + + EnsureMinSizeInDefinitionRange( + key.U ? DefinitionsU : DefinitionsV, + key.Start, + key.Count, + requestedSize, + key.U ? referenceSize.Width : referenceSize.Height); + } + } + } + + /// + /// Helper method to register a span information for delayed processing. + /// + /// Reference to a hashtable object used as storage. + /// Span starting index. + /// Span count. + /// true if this is a column span. false if this is a row span. + /// Value to store. If an entry already exists the biggest value is stored. + private static void RegisterSpan( + ref Hashtable store, + int start, + int count, + bool u, + double value) + { + if (store == null) + { + store = new Hashtable(); + } + + SpanKey key = new SpanKey(start, count, u); + object o = store[key]; + + if (o == null + || value > (double)o) + { + store[key] = value; + } + } + + /// + /// Takes care of measuring a single cell. + /// + /// Index of the cell to measure. + /// If "true" then cell is always + /// calculated to infinite height. + private void MeasureCell( + int cell, + bool forceInfinityV) + { + + + double cellMeasureWidth; + double cellMeasureHeight; + + if (PrivateCells[cell].IsAutoU + && !PrivateCells[cell].IsStarU) + { + // if cell belongs to at least one Auto column and not a single Star column + // then it should be calculated "to content", thus it is possible to "shortcut" + // calculations and simply assign PositiveInfinity here. + cellMeasureWidth = double.PositiveInfinity; + } + else + { + // otherwise... + cellMeasureWidth = GetMeasureSizeForRange( + DefinitionsU, + PrivateCells[cell].ColumnIndex, + PrivateCells[cell].ColumnSpan); + } + + if (forceInfinityV) + { + cellMeasureHeight = double.PositiveInfinity; + } + else if (PrivateCells[cell].IsAutoV + && !PrivateCells[cell].IsStarV) + { + // if cell belongs to at least one Auto row and not a single Star row + // then it should be calculated "to content", thus it is possible to "shortcut" + // calculations and simply assign PositiveInfinity here. + cellMeasureHeight = double.PositiveInfinity; + } + else + { + cellMeasureHeight = GetMeasureSizeForRange( + DefinitionsV, + PrivateCells[cell].RowIndex, + PrivateCells[cell].RowSpan); + } + + + var child = this.Children[cell]; + if (child != null) + { + Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); + child.Measure(childConstraint); + } + + + + } + /// - /// Defines the Column attached property. + /// Calculates one dimensional measure size for given definitions' range. /// - public static readonly AttachedProperty ColumnProperty = - AvaloniaProperty.RegisterAttached( - "Column", - validate: ValidateColumn); + /// Source array of definitions to read values from. + /// Starting index of the range. + /// Number of definitions included in the range. + /// Calculated measure size. + /// + /// For "Auto" definitions MinWidth is used in place of PreferredSize. + /// + private double GetMeasureSizeForRange( + IReadOnlyList definitions, + int start, + int count) + { + Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Count); + + double measureSize = 0; + int i = start + count - 1; + + do + { + measureSize += (definitions[i].SizeType == LayoutTimeSizeType.Auto) + ? definitions[i].MinSize + : definitions[i].MeasureSize; + } while (--i >= start); + + return (measureSize); + } /// - /// Defines the ColumnSpan attached property. + /// Accumulates length type information for given definition's range. /// - public static readonly AttachedProperty ColumnSpanProperty = - AvaloniaProperty.RegisterAttached("ColumnSpan", 1); + /// Source array of definitions to read values from. + /// Starting index of the range. + /// Number of definitions included in the range. + /// Length type for given range. + private LayoutTimeSizeType GetLengthTypeForRange( + IReadOnlyList definitions, + int start, + int count) + { + Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Count); + + LayoutTimeSizeType lengthType = LayoutTimeSizeType.None; + int i = start + count - 1; + + do + { + lengthType |= definitions[i].SizeType; + } while (--i >= start); + + return (lengthType); + } /// - /// Defines the Row attached property. + /// Distributes min size back to definition array's range. /// - public static readonly AttachedProperty RowProperty = - AvaloniaProperty.RegisterAttached( - "Row", - validate: ValidateRow); + /// Start of the range. + /// Number of items in the range. + /// Minimum size that should "fit" into the definitions range. + /// Definition array receiving distribution. + /// Size used to resolve percentages. + private void EnsureMinSizeInDefinitionRange( + IReadOnlyList definitions, + int start, + int count, + double requestedSize, + double percentReferenceSize) + { + Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Count); + + // avoid processing when asked to distribute "0" + if (!_IsZero(requestedSize)) + { + DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting + int end = start + count; + int autoDefinitionsCount = 0; + double rangeMinSize = 0; + double rangePreferredSize = 0; + double rangeMaxSize = 0; + double maxMaxSize = 0; // maximum of maximum sizes + + // first accumulate the necessary information: + // a) sum up the sizes in the range; + // b) count the number of auto definitions in the range; + // c) initialize temp array + // d) cache the maximum size into SizeCache + // e) accumulate max of max sizes + for (int i = start; i < end; ++i) + { + double minSize = definitions[i].MinSize; + double preferredSize = definitions[i].PreferredSize; + double maxSize = Math.Max(definitions[i].UserMaxSize, minSize); + + rangeMinSize += minSize; + rangePreferredSize += preferredSize; + rangeMaxSize += maxSize; + + definitions[i].SizeCache = maxSize; + + // sanity check: no matter what, but min size must always be the smaller; + // max size must be the biggest; and preferred should be in between + Debug.Assert(minSize <= preferredSize + && preferredSize <= maxSize + && rangeMinSize <= rangePreferredSize + && rangePreferredSize <= rangeMaxSize); + + if (maxMaxSize < maxSize) maxMaxSize = maxSize; + if (definitions[i].UserSize.IsAuto) autoDefinitionsCount++; + tempDefinitions[i - start] = definitions[i]; + } + + // avoid processing if the range already big enough + if (requestedSize > rangeMinSize) + { + if (requestedSize <= rangePreferredSize) + { + // + // requestedSize fits into preferred size of the range. + // distribute according to the following logic: + // * do not distribute into auto definitions - they should continue to stay "tight"; + // * for all non-auto definitions distribute to equi-size min sizes, without exceeding preferred size. + // + // in order to achieve that, definitions are sorted in a way that all auto definitions + // are first, then definitions follow ascending order with PreferredSize as the key of sorting. + // + double sizeToDistribute; + int i; + + Array.Sort(tempDefinitions, 0, count, s_spanPreferredDistributionOrderComparer); + for (i = 0, sizeToDistribute = requestedSize; i < autoDefinitionsCount; ++i) + { + // sanity check: only auto definitions allowed in this loop + Debug.Assert(tempDefinitions[i].UserSize.IsAuto); + + // adjust sizeToDistribute value by subtracting auto definition min size + sizeToDistribute -= (tempDefinitions[i].MinSize); + } + + for (; i < count; ++i) + { + // sanity check: no auto definitions allowed in this loop + Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); + + double newMinSize = Math.Min(sizeToDistribute / (count - i), tempDefinitions[i].PreferredSize); + if (newMinSize > tempDefinitions[i].MinSize) { tempDefinitions[i].UpdateMinSize(newMinSize); } + sizeToDistribute -= newMinSize; + } + + // sanity check: requested size must all be distributed + Debug.Assert(_IsZero(sizeToDistribute)); + } + else if (requestedSize <= rangeMaxSize) + { + // + // requestedSize bigger than preferred size, but fit into max size of the range. + // distribute according to the following logic: + // * do not distribute into auto definitions, if possible - they should continue to stay "tight"; + // * for all non-auto definitions distribute to euqi-size min sizes, without exceeding max size. + // + // in order to achieve that, definitions are sorted in a way that all non-auto definitions + // are last, then definitions follow ascending order with MaxSize as the key of sorting. + // + double sizeToDistribute; + int i; + + Array.Sort(tempDefinitions, 0, count, s_spanMaxDistributionOrderComparer); + for (i = 0, sizeToDistribute = requestedSize - rangePreferredSize; i < count - autoDefinitionsCount; ++i) + { + // sanity check: no auto definitions allowed in this loop + Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); + + double preferredSize = tempDefinitions[i].PreferredSize; + double newMinSize = preferredSize + sizeToDistribute / (count - autoDefinitionsCount - i); + tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); + sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); + } + + for (; i < count; ++i) + { + // sanity check: only auto definitions allowed in this loop + Debug.Assert(tempDefinitions[i].UserSize.IsAuto); + + double preferredSize = tempDefinitions[i].MinSize; + double newMinSize = preferredSize + sizeToDistribute / (count - i); + tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); + sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); + } + + // sanity check: requested size must all be distributed + Debug.Assert(_IsZero(sizeToDistribute)); + } + else + { + // + // requestedSize bigger than max size of the range. + // distribute according to the following logic: + // * for all definitions distribute to equi-size min sizes. + // + double equalSize = requestedSize / count; + + if (equalSize < maxMaxSize + && !_AreClose(equalSize, maxMaxSize)) + { + // equi-size is less than maximum of maxSizes. + // in this case distribute so that smaller definitions grow faster than + // bigger ones. + double totalRemainingSize = maxMaxSize * count - rangeMaxSize; + double sizeToDistribute = requestedSize - rangeMaxSize; + + // sanity check: totalRemainingSize and sizeToDistribute must be real positive numbers + Debug.Assert(!double.IsInfinity(totalRemainingSize) + && !double.IsNaN(totalRemainingSize) + && totalRemainingSize > 0 + && !double.IsInfinity(sizeToDistribute) + && !double.IsNaN(sizeToDistribute) + && sizeToDistribute > 0); + + for (int i = 0; i < count; ++i) + { + double deltaSize = (maxMaxSize - tempDefinitions[i].SizeCache) * sizeToDistribute / totalRemainingSize; + tempDefinitions[i].UpdateMinSize(tempDefinitions[i].SizeCache + deltaSize); + } + } + else + { + // + // equi-size is greater or equal to maximum of max sizes. + // all definitions receive equalSize as their mim sizes. + // + for (int i = 0; i < count; ++i) + { + tempDefinitions[i].UpdateMinSize(equalSize); + } + } + } + } + } + } /// - /// Defines the RowSpan attached property. + /// Resolves Star's for given array of definitions. /// - public static readonly AttachedProperty RowSpanProperty = - AvaloniaProperty.RegisterAttached("RowSpan", 1); + /// Array of definitions to resolve stars. + /// All available size. + /// + /// Must initialize LayoutSize for all Star entries in given array of definitions. + /// + private void ResolveStar( + IReadOnlyList definitions, + double availableSize) + { + ResolveStarMaxDiscrepancy(definitions, availableSize); + } - public static readonly AttachedProperty IsSharedSizeScopeProperty = - AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); + // New implementation as of 4.7. Several improvements: + // 1. Allocate to *-defs hitting their min or max constraints, before allocating + // to other *-defs. A def that hits its min uses more space than its + // proportional share, reducing the space available to everyone else. + // The legacy algorithm deducted this space only from defs processed + // after the min; the new algorithm deducts it proportionally from all + // defs. This avoids the "*-defs exceed available space" problem, + // and other related problems where *-defs don't receive proportional + // allocations even though no constraints are preventing it. + // 2. When multiple defs hit min or max, resolve the one with maximum + // discrepancy (defined below). This avoids discontinuities - small + // change in available space resulting in large change to one def's allocation. + // 3. Correct handling of large *-values, including Infinity. + private void ResolveStarMaxDiscrepancy( + IReadOnlyList definitions, + double availableSize) + { + int defCount = definitions.Count; + DefinitionBase[] tempDefinitions = TempDefinitions; + int minCount = 0, maxCount = 0; + double takenSize = 0; + double totalStarWeight = 0.0; + int starCount = 0; // number of unresolved *-definitions + double scale = 1.0; // scale factor applied to each *-weight; negative means "Infinity is present" + + // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights + double maxStar = 0.0; + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.SizeType == LayoutTimeSizeType.Star) + { + ++starCount; + def.MeasureSize = 1.0; // meaning "not yet resolved in phase 3" + if (def.UserSize.Value > maxStar) + { + maxStar = def.UserSize.Value; + } + } + } + + if (Double.IsPositiveInfinity(maxStar)) + { + // negative scale means one or more of the weights was Infinity + scale = -1.0; + } + else if (starCount > 0) + { + // if maxStar * starCount > Double.Max, summing all the weights could cause + // floating-point overflow. To avoid that, scale the weights by a factor to keep + // the sum within limits. Choose a power of 2, to preserve precision. + double power = Math.Floor(Math.Log(Double.MaxValue / maxStar / starCount, 2.0)); + if (power < 0.0) + { + scale = Math.Pow(2.0, power - 4.0); // -4 is just for paranoia + } + } + + // normally Phases 2 and 3 execute only once. But certain unusual combinations of weights + // and constraints can defeat the algorithm, in which case we repeat Phases 2 and 3. + // More explanation below... + for (bool runPhase2and3 = true; runPhase2and3;) + { + // Phase 2. Compute total *-weight W and available space S. + // For *-items that have Min or Max constraints, compute the ratios used to decide + // whether proportional space is too big or too small and add the item to the + // corresponding list. (The "min" list is in the first half of tempDefinitions, + // the "max" list in the second half. TempDefinitions has capacity at least + // 2*defCount, so there's room for both lists.) + totalStarWeight = 0.0; + takenSize = 0.0; + minCount = maxCount = 0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + switch (def.SizeType) + { + case (LayoutTimeSizeType.Auto): + takenSize += definitions[i].MinSize; + break; + case (LayoutTimeSizeType.Pixel): + takenSize += def.MeasureSize; + break; + case (LayoutTimeSizeType.Star): + if (def.MeasureSize < 0.0) + { + takenSize += -def.MeasureSize; // already resolved + } + else + { + double starWeight = StarWeight(def, scale); + totalStarWeight += starWeight; + + if (def.MinSize > 0.0) + { + // store ratio w/min in MeasureSize (for now) + tempDefinitions[minCount++] = def; + def.MeasureSize = starWeight / def.MinSize; + } + + double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); + if (!Double.IsPositiveInfinity(effectiveMaxSize)) + { + // store ratio w/max in SizeCache (for now) + tempDefinitions[defCount + maxCount++] = def; + def.SizeCache = starWeight / effectiveMaxSize; + } + } + break; + } + } + + // Phase 3. Resolve *-items whose proportional sizes are too big or too small. + int minCountPhase2 = minCount, maxCountPhase2 = maxCount; + double takenStarWeight = 0.0; + double remainingAvailableSize = availableSize - takenSize; + double remainingStarWeight = totalStarWeight - takenStarWeight; + Array.Sort(tempDefinitions, 0, minCount, s_minRatioComparer); + Array.Sort(tempDefinitions, defCount, maxCount, s_maxRatioComparer); + + while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) + { + // the calculation + // remainingStarWeight = totalStarWeight - takenStarWeight + // is subject to catastrophic cancellation if the two terms are nearly equal, + // which leads to meaningless results. Check for that, and recompute from + // the remaining definitions. [This leads to quadratic behavior in really + // pathological cases - but they'd never arise in practice.] + const double starFactor = 1.0 / 256.0; // lose more than 8 bits of precision -> recalculate + if (remainingStarWeight < totalStarWeight * starFactor) + { + takenStarWeight = 0.0; + totalStarWeight = 0.0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + if (def.SizeType == LayoutTimeSizeType.Star && def.MeasureSize > 0.0) + { + totalStarWeight += StarWeight(def, scale); + } + } + + remainingStarWeight = totalStarWeight - takenStarWeight; + } + + double minRatio = (minCount > 0) ? tempDefinitions[minCount - 1].MeasureSize : Double.PositiveInfinity; + double maxRatio = (maxCount > 0) ? tempDefinitions[defCount + maxCount - 1].SizeCache : -1.0; + + // choose the def with larger ratio to the current proportion ("max discrepancy") + double proportion = remainingStarWeight / remainingAvailableSize; + bool? chooseMin = Choose(minRatio, maxRatio, proportion); + + // if no def was chosen, advance to phase 4; the current proportion doesn't + // conflict with any min or max values. + if (!(chooseMin.HasValue)) + { + break; + } + + // get the chosen definition and its resolved size + DefinitionBase resolvedDef; + double resolvedSize; + if (chooseMin == true) + { + resolvedDef = tempDefinitions[minCount - 1]; + resolvedSize = resolvedDef.MinSize; + --minCount; + } + else + { + resolvedDef = tempDefinitions[defCount + maxCount - 1]; + resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); + --maxCount; + } + + // resolve the chosen def, deduct its contributions from W and S. + // Defs resolved in phase 3 are marked by storing the negative of their resolved + // size in MeasureSize, to distinguish them from a pending def. + takenSize += resolvedSize; + resolvedDef.MeasureSize = -resolvedSize; + takenStarWeight += StarWeight(resolvedDef, scale); + --starCount; + + remainingAvailableSize = availableSize - takenSize; + remainingStarWeight = totalStarWeight - takenStarWeight; + + // advance to the next candidate defs, removing ones that have been resolved. + // Both counts are advanced, as a def might appear in both lists. + while (minCount > 0 && tempDefinitions[minCount - 1].MeasureSize < 0.0) + { + --minCount; + tempDefinitions[minCount] = null; + } + while (maxCount > 0 && tempDefinitions[defCount + maxCount - 1].MeasureSize < 0.0) + { + --maxCount; + tempDefinitions[defCount + maxCount] = null; + } + } + + // decide whether to run Phase2 and Phase3 again. There are 3 cases: + // 1. There is space available, and *-defs remaining. This is the + // normal case - move on to Phase 4 to allocate the remaining + // space proportionally to the remaining *-defs. + // 2. There is space available, but no *-defs. This implies at least one + // def was resolved as 'max', taking less space than its proportion. + // If there are also 'min' defs, reconsider them - we can give + // them more space. If not, all the *-defs are 'max', so there's + // no way to use all the available space. + // 3. We allocated too much space. This implies at least one def was + // resolved as 'min'. If there are also 'max' defs, reconsider + // them, otherwise the over-allocation is an inevitable consequence + // of the given min constraints. + // Note that if we return to Phase2, at least one *-def will have been + // resolved. This guarantees we don't run Phase2+3 infinitely often. + runPhase2and3 = false; + if (starCount == 0 && takenSize < availableSize) + { + // if no *-defs remain and we haven't allocated all the space, reconsider the defs + // resolved as 'min'. Their allocation can be increased to make up the gap. + for (int i = minCount; i < minCountPhase2; ++i) + { + DefinitionBase def = tempDefinitions[i]; + if (def != null) + { + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + + if (takenSize > availableSize) + { + // if we've allocated too much space, reconsider the defs + // resolved as 'max'. Their allocation can be decreased to make up the gap. + for (int i = maxCount; i < maxCountPhase2; ++i) + { + DefinitionBase def = tempDefinitions[defCount + i]; + if (def != null) + { + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + } + + // Phase 4. Resolve the remaining defs proportionally. + starCount = 0; + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.SizeType == LayoutTimeSizeType.Star) + { + if (def.MeasureSize < 0.0) + { + // this def was resolved in phase 3 - fix up its measure size + def.MeasureSize = -def.MeasureSize; + } + else + { + // this def needs resolution, add it to the list, sorted by *-weight + tempDefinitions[starCount++] = def; + def.MeasureSize = StarWeight(def, scale); + } + } + } + + if (starCount > 0) + { + Array.Sort(tempDefinitions, 0, starCount, s_starWeightComparer); + + // compute the partial sums of *-weight, in increasing order of weight + // for minimal loss of precision. + totalStarWeight = 0.0; + for (int i = 0; i < starCount; ++i) + { + DefinitionBase def = tempDefinitions[i]; + totalStarWeight += def.MeasureSize; + def.SizeCache = totalStarWeight; + } + + // resolve the defs, in decreasing order of weight + for (int i = starCount - 1; i >= 0; --i) + { + DefinitionBase def = tempDefinitions[i]; + double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(availableSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; + + // min and max should have no effect by now, but just in case... + resolvedSize = Math.Min(resolvedSize, def.UserMaxSize); + resolvedSize = Math.Max(def.MinSize, resolvedSize); + + def.MeasureSize = resolvedSize; + takenSize += resolvedSize; + } + } + } + + /// + /// Calculates desired size for given array of definitions. + /// + /// Array of definitions to use for calculations. + /// Desired size. + private double CalculateDesiredSize( + IReadOnlyList definitions) + { + double desiredSize = 0; + + for (int i = 0; i < definitions.Count; ++i) + { + desiredSize += definitions[i].MinSize; + } + + return (desiredSize); + } + + /// + /// Calculates and sets final size for all definitions in the given array. + /// + /// Array of definitions to process. + /// Final size to lay out to. + /// True if sizing column definitions, false for rows + private void SetFinalSize( + IReadOnlyList definitions, + double finalSize, + bool columns) + { + SetFinalSizeMaxDiscrepancy(definitions, finalSize, columns); + } + + // new implementation, as of 4.7. This incorporates the same algorithm + // as in ResolveStarMaxDiscrepancy. It differs in the same way that SetFinalSizeLegacy + // differs from ResolveStarLegacy, namely (a) leaves results in def.SizeCache + // instead of def.MeasureSize, (b) implements LayoutRounding if requested, + // (c) stores intermediate results differently. + // The LayoutRounding logic is improved: + // 1. Use pre-rounded values during proportional allocation. This avoids the + // same kind of problems arising from interaction with min/max that + // motivated the new algorithm in the first place. + // 2. Use correct "nudge" amount when distributing roundoff space. This + // comes into play at high DPI - greater than 134. + // 3. Applies rounding only to real pixel values (not to ratios) + private void SetFinalSizeMaxDiscrepancy( + IReadOnlyList definitions, + double finalSize, + bool columns) + { + int defCount = definitions.Count; + int[] definitionIndices = DefinitionIndices; + int minCount = 0, maxCount = 0; + double takenSize = 0.0; + double totalStarWeight = 0.0; + int starCount = 0; // number of unresolved *-definitions + double scale = 1.0; // scale factor applied to each *-weight; negative means "Infinity is present" + + // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights + double maxStar = 0.0; + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.UserSize.IsStar) + { + ++starCount; + def.MeasureSize = 1.0; // meaning "not yet resolved in phase 3" + if (def.UserSize.Value > maxStar) + { + maxStar = def.UserSize.Value; + } + } + } + + if (Double.IsPositiveInfinity(maxStar)) + { + // negative scale means one or more of the weights was Infinity + scale = -1.0; + } + else if (starCount > 0) + { + // if maxStar * starCount > Double.Max, summing all the weights could cause + // floating-point overflow. To avoid that, scale the weights by a factor to keep + // the sum within limits. Choose a power of 2, to preserve precision. + double power = Math.Floor(Math.Log(Double.MaxValue / maxStar / starCount, 2.0)); + if (power < 0.0) + { + scale = Math.Pow(2.0, power - 4.0); // -4 is just for paranoia + } + } + + // normally Phases 2 and 3 execute only once. But certain unusual combinations of weights + // and constraints can defeat the algorithm, in which case we repeat Phases 2 and 3. + // More explanation below... + for (bool runPhase2and3 = true; runPhase2and3;) + { + // Phase 2. Compute total *-weight W and available space S. + // For *-items that have Min or Max constraints, compute the ratios used to decide + // whether proportional space is too big or too small and add the item to the + // corresponding list. (The "min" list is in the first half of definitionIndices, + // the "max" list in the second half. DefinitionIndices has capacity at least + // 2*defCount, so there's room for both lists.) + totalStarWeight = 0.0; + takenSize = 0.0; + minCount = maxCount = 0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.UserSize.IsStar) + { + Debug.Assert(!def.IsShared, "*-defs cannot be shared"); + + if (def.MeasureSize < 0.0) + { + takenSize += -def.MeasureSize; // already resolved + } + else + { + double starWeight = StarWeight(def, scale); + totalStarWeight += starWeight; + + if (def.MinSizeForArrange > 0.0) + { + // store ratio w/min in MeasureSize (for now) + definitionIndices[minCount++] = i; + def.MeasureSize = starWeight / def.MinSizeForArrange; + } + + double effectiveMaxSize = Math.Max(def.MinSizeForArrange, def.UserMaxSize); + if (!Double.IsPositiveInfinity(effectiveMaxSize)) + { + // store ratio w/max in SizeCache (for now) + definitionIndices[defCount + maxCount++] = i; + def.SizeCache = starWeight / effectiveMaxSize; + } + } + } + else + { + double userSize = 0; + + switch (def.UserSize.GridUnitType) + { + case (GridUnitType.Pixel): + userSize = def.UserSize.Value; + break; + + case (GridUnitType.Auto): + userSize = def.MinSizeForArrange; + break; + } + + double userMaxSize; + + if (def.IsShared) + { + // overriding userMaxSize effectively prevents squishy-ness. + // this is a "solution" to avoid shared definitions from been sized to + // different final size at arrange time, if / when different grids receive + // different final sizes. + userMaxSize = userSize; + } + else + { + userMaxSize = def.UserMaxSize; + } + + def.SizeCache = Math.Max(def.MinSizeForArrange, Math.Min(userSize, userMaxSize)); + takenSize += def.SizeCache; + } + } + + // Phase 3. Resolve *-items whose proportional sizes are too big or too small. + int minCountPhase2 = minCount, maxCountPhase2 = maxCount; + double takenStarWeight = 0.0; + double remainingAvailableSize = finalSize - takenSize; + double remainingStarWeight = totalStarWeight - takenStarWeight; + + MinRatioIndexComparer minRatioIndexComparer = new MinRatioIndexComparer(definitions); + Array.Sort(definitionIndices, 0, minCount, minRatioIndexComparer); + MaxRatioIndexComparer maxRatioIndexComparer = new MaxRatioIndexComparer(definitions); + Array.Sort(definitionIndices, defCount, maxCount, maxRatioIndexComparer); + + while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) + { + // the calculation + // remainingStarWeight = totalStarWeight - takenStarWeight + // is subject to catastrophic cancellation if the two terms are nearly equal, + // which leads to meaningless results. Check for that, and recompute from + // the remaining definitions. [This leads to quadratic behavior in really + // pathological cases - but they'd never arise in practice.] + const double starFactor = 1.0 / 256.0; // lose more than 8 bits of precision -> recalculate + if (remainingStarWeight < totalStarWeight * starFactor) + { + takenStarWeight = 0.0; + totalStarWeight = 0.0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + if (def.UserSize.IsStar && def.MeasureSize > 0.0) + { + totalStarWeight += StarWeight(def, scale); + } + } + + remainingStarWeight = totalStarWeight - takenStarWeight; + } + + double minRatio = (minCount > 0) ? definitions[definitionIndices[minCount - 1]].MeasureSize : Double.PositiveInfinity; + double maxRatio = (maxCount > 0) ? definitions[definitionIndices[defCount + maxCount - 1]].SizeCache : -1.0; + + // choose the def with larger ratio to the current proportion ("max discrepancy") + double proportion = remainingStarWeight / remainingAvailableSize; + bool? chooseMin = Choose(minRatio, maxRatio, proportion); + + // if no def was chosen, advance to phase 4; the current proportion doesn't + // conflict with any min or max values. + if (!chooseMin.HasValue) + { + break; + } + + // get the chosen definition and its resolved size + int resolvedIndex; + DefinitionBase resolvedDef; + double resolvedSize; + if (chooseMin == true) + { + resolvedIndex = definitionIndices[minCount - 1]; + resolvedDef = definitions[resolvedIndex]; + resolvedSize = resolvedDef.MinSizeForArrange; + --minCount; + } + else + { + resolvedIndex = definitionIndices[defCount + maxCount - 1]; + resolvedDef = definitions[resolvedIndex]; + resolvedSize = Math.Max(resolvedDef.MinSizeForArrange, resolvedDef.UserMaxSize); + --maxCount; + } + + // resolve the chosen def, deduct its contributions from W and S. + // Defs resolved in phase 3 are marked by storing the negative of their resolved + // size in MeasureSize, to distinguish them from a pending def. + takenSize += resolvedSize; + resolvedDef.MeasureSize = -resolvedSize; + takenStarWeight += StarWeight(resolvedDef, scale); + --starCount; + + remainingAvailableSize = finalSize - takenSize; + remainingStarWeight = totalStarWeight - takenStarWeight; + + // advance to the next candidate defs, removing ones that have been resolved. + // Both counts are advanced, as a def might appear in both lists. + while (minCount > 0 && definitions[definitionIndices[minCount - 1]].MeasureSize < 0.0) + { + --minCount; + definitionIndices[minCount] = -1; + } + while (maxCount > 0 && definitions[definitionIndices[defCount + maxCount - 1]].MeasureSize < 0.0) + { + --maxCount; + definitionIndices[defCount + maxCount] = -1; + } + } + + // decide whether to run Phase2 and Phase3 again. There are 3 cases: + // 1. There is space available, and *-defs remaining. This is the + // normal case - move on to Phase 4 to allocate the remaining + // space proportionally to the remaining *-defs. + // 2. There is space available, but no *-defs. This implies at least one + // def was resolved as 'max', taking less space than its proportion. + // If there are also 'min' defs, reconsider them - we can give + // them more space. If not, all the *-defs are 'max', so there's + // no way to use all the available space. + // 3. We allocated too much space. This implies at least one def was + // resolved as 'min'. If there are also 'max' defs, reconsider + // them, otherwise the over-allocation is an inevitable consequence + // of the given min constraints. + // Note that if we return to Phase2, at least one *-def will have been + // resolved. This guarantees we don't run Phase2+3 infinitely often. + runPhase2and3 = false; + if (starCount == 0 && takenSize < finalSize) + { + // if no *-defs remain and we haven't allocated all the space, reconsider the defs + // resolved as 'min'. Their allocation can be increased to make up the gap. + for (int i = minCount; i < minCountPhase2; ++i) + { + if (definitionIndices[i] >= 0) + { + DefinitionBase def = definitions[definitionIndices[i]]; + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + + if (takenSize > finalSize) + { + // if we've allocated too much space, reconsider the defs + // resolved as 'max'. Their allocation can be decreased to make up the gap. + for (int i = maxCount; i < maxCountPhase2; ++i) + { + if (definitionIndices[defCount + i] >= 0) + { + DefinitionBase def = definitions[definitionIndices[defCount + i]]; + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + } + + // Phase 4. Resolve the remaining defs proportionally. + starCount = 0; + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.UserSize.IsStar) + { + if (def.MeasureSize < 0.0) + { + // this def was resolved in phase 3 - fix up its size + def.SizeCache = -def.MeasureSize; + } + else + { + // this def needs resolution, add it to the list, sorted by *-weight + definitionIndices[starCount++] = i; + def.MeasureSize = StarWeight(def, scale); + } + } + } + + if (starCount > 0) + { + StarWeightIndexComparer starWeightIndexComparer = new StarWeightIndexComparer(definitions); + Array.Sort(definitionIndices, 0, starCount, starWeightIndexComparer); + + // compute the partial sums of *-weight, in increasing order of weight + // for minimal loss of precision. + totalStarWeight = 0.0; + for (int i = 0; i < starCount; ++i) + { + DefinitionBase def = definitions[definitionIndices[i]]; + totalStarWeight += def.MeasureSize; + def.SizeCache = totalStarWeight; + } + + // resolve the defs, in decreasing order of weight. + for (int i = starCount - 1; i >= 0; --i) + { + DefinitionBase def = definitions[definitionIndices[i]]; + double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(finalSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; + + // min and max should have no effect by now, but just in case... + resolvedSize = Math.Min(resolvedSize, def.UserMaxSize); + resolvedSize = Math.Max(def.MinSizeForArrange, resolvedSize); + + // Use the raw (unrounded) sizes to update takenSize, so that + // proportions are computed in the same terms as in phase 3; + // this avoids errors arising from min/max constraints. + takenSize += resolvedSize; + def.SizeCache = resolvedSize; + } + } + + // Phase 5. Apply layout rounding. We do this after fully allocating + // unrounded sizes, to avoid breaking assumptions in the previous phases + if (UseLayoutRounding) + { + // DpiScale dpiScale = GetDpi(); + // double dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY; + var dpi = (VisualRoot as Layout.ILayoutRoot)?.LayoutScaling ?? 1.0; + double[] roundingErrors = RoundingErrors; + double roundedTakenSize = 0.0; + + // round each of the allocated sizes, keeping track of the deltas + for (int i = 0; i < definitions.Count; ++i) + { + DefinitionBase def = definitions[i]; + double roundedSize = MathUtilities.RoundLayoutValue(def.SizeCache, dpi); + roundingErrors[i] = (roundedSize - def.SizeCache); + def.SizeCache = roundedSize; + roundedTakenSize += roundedSize; + } + + // The total allocation might differ from finalSize due to rounding + // effects. Tweak the allocations accordingly. + + // Theoretical and historical note. The problem at hand - allocating + // space to columns (or rows) with *-weights, min and max constraints, + // and layout rounding - has a long history. Especially the special + // case of 50 columns with min=1 and available space=435 - allocating + // seats in the U.S. House of Representatives to the 50 states in + // proportion to their population. There are numerous algorithms + // and papers dating back to the 1700's, including the book: + // Balinski, M. and H. Young, Fair Representation, Yale University Press, New Haven, 1982. + // + // One surprising result of all this research is that *any* algorithm + // will suffer from one or more undesirable features such as the + // "population paradox" or the "Alabama paradox", where (to use our terminology) + // increasing the available space by one pixel might actually decrease + // the space allocated to a given column, or increasing the weight of + // a column might decrease its allocation. This is worth knowing + // in case someone complains about this behavior; it's not a bug so + // much as something inherent to the problem. Cite the book mentioned + // above or one of the 100s of references, and resolve as WontFix. + // + // Fortunately, our scenarios tend to have a small number of columns (~10 or fewer) + // each being allocated a large number of pixels (~50 or greater), and + // people don't even notice the kind of 1-pixel anomolies that are + // theoretically inevitable, or don't care if they do. At least they shouldn't + // care - no one should be using the results WPF's grid layout to make + // quantitative decisions; its job is to produce a reasonable display, not + // to allocate seats in Congress. + // + // Our algorithm is more susceptible to paradox than the one currently + // used for Congressional allocation ("Huntington-Hill" algorithm), but + // it is faster to run: O(N log N) vs. O(S * N), where N=number of + // definitions, S = number of available pixels. And it produces + // adequate results in practice, as mentioned above. + // + // To reiterate one point: all this only applies when layout rounding + // is in effect. When fractional sizes are allowed, the algorithm + // behaves as well as possible, subject to the min/max constraints + // and precision of floating-point computation. (However, the resulting + // display is subject to anti-aliasing problems. TANSTAAFL.) + + if (!_AreClose(roundedTakenSize, finalSize)) + { + // Compute deltas + for (int i = 0; i < definitions.Count; ++i) + { + definitionIndices[i] = i; + } + + // Sort rounding errors + RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); + Array.Sort(definitionIndices, 0, definitions.Count, roundingErrorIndexComparer); + double adjustedSize = roundedTakenSize; + double dpiIncrement = 1.0 / dpi; + + if (roundedTakenSize > finalSize) + { + int i = definitions.Count - 1; + while ((adjustedSize > finalSize && !_AreClose(adjustedSize, finalSize)) && i >= 0) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache - dpiIncrement; + final = Math.Max(final, definition.MinSizeForArrange); + if (final < definition.SizeCache) + { + adjustedSize -= dpiIncrement; + } + definition.SizeCache = final; + i--; + } + } + else if (roundedTakenSize < finalSize) + { + int i = 0; + while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Count) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache + dpiIncrement; + final = Math.Max(final, definition.MinSizeForArrange); + if (final > definition.SizeCache) + { + adjustedSize += dpiIncrement; + } + definition.SizeCache = final; + i++; + } + } + } + } + + // Phase 6. Compute final offsets + definitions[0].FinalOffset = 0.0; + for (int i = 0; i < definitions.Count; ++i) + { + definitions[(i + 1) % definitions.Count].FinalOffset = definitions[i].FinalOffset + definitions[i].SizeCache; + } + } + + /// + /// Choose the ratio with maximum discrepancy from the current proportion. + /// Returns: + /// true if proportion fails a min constraint but not a max, or + /// if the min constraint has higher discrepancy + /// false if proportion fails a max constraint but not a min, or + /// if the max constraint has higher discrepancy + /// null if proportion doesn't fail a min or max constraint + /// The discrepancy is the ratio of the proportion to the max- or min-ratio. + /// When both ratios hit the constraint, minRatio < proportion < maxRatio, + /// and the minRatio has higher discrepancy if + /// (proportion / minRatio) > (maxRatio / proportion) + /// + private static bool? Choose(double minRatio, double maxRatio, double proportion) + { + if (minRatio < proportion) + { + if (maxRatio > proportion) + { + // compare proportion/minRatio : maxRatio/proportion, but + // do it carefully to avoid floating-point overflow or underflow + // and divide-by-0. + double minPower = Math.Floor(Math.Log(minRatio, 2.0)); + double maxPower = Math.Floor(Math.Log(maxRatio, 2.0)); + double f = Math.Pow(2.0, Math.Floor((minPower + maxPower) / 2.0)); + if ((proportion / f) * (proportion / f) > (minRatio / f) * (maxRatio / f)) + { + return true; + } + else + { + return false; + } + } + else + { + return true; + } + } + else if (maxRatio > proportion) + { + return false; + } + + return null; + } + + /// + /// Sorts row/column indices by rounding error if layout rounding is applied. + /// + /// Index, rounding error pair + /// Index, rounding error pair + /// 1 if x.Value > y.Value, 0 if equal, -1 otherwise + private static int CompareRoundingErrors(KeyValuePair x, KeyValuePair y) + { + if (x.Value < y.Value) + { + return -1; + } + else if (x.Value > y.Value) + { + return 1; + } + return 0; + } + + /// + /// Calculates final (aka arrange) size for given range. + /// + /// Array of definitions to process. + /// Start of the range. + /// Number of items in the range. + /// Final size. + private double GetFinalSizeForRange( + IReadOnlyList definitions, + int start, + int count) + { + double size = 0; + int i = start + count - 1; + + do + { + size += definitions[i].SizeCache; + } while (--i >= start); + + return (size); + } + + /// + /// Clears dirty state for the grid and its columns / rows + /// + private void SetValid() + { + ExtendedData extData = ExtData; + if (extData != null) + { + // for (int i = 0; i < PrivateColumnCount; ++i) DefinitionsU[i].SetValid (); + // for (int i = 0; i < PrivateRowCount; ++i) DefinitionsV[i].SetValid (); + + if (extData.TempDefinitions != null) + { + // TempDefinitions has to be cleared to avoid "memory leaks" + Array.Clear(extData.TempDefinitions, 0, Math.Max(DefinitionsU.Count, DefinitionsV.Count)); + extData.TempDefinitions = null; + } + } + } + + /// + /// Synchronized ShowGridLines property with the state of the grid's visual collection + /// by adding / removing GridLinesRenderer visual. + /// Returns a reference to GridLinesRenderer visual or null. + /// + private GridLinesRenderer EnsureGridLinesRenderer() + { + // + // synchronize the state + // + if (ShowGridLines && (_gridLinesRenderer == null)) + { + _gridLinesRenderer = new GridLinesRenderer(); + this.VisualChildren.Add(_gridLinesRenderer); + } + + if ((!ShowGridLines) && (_gridLinesRenderer != null)) + { + this.VisualChildren.Add(_gridLinesRenderer); + _gridLinesRenderer = null; + } + + return (_gridLinesRenderer); + } + + /// + /// SetFlags is used to set or unset one or multiple + /// flags on the object. + /// + private void SetFlags(bool value, Flags flags) + { + _flags = value ? (_flags | flags) : (_flags & (~flags)); + } + + /// + /// CheckFlagsAnd returns true if all the flags in the + /// given bitmask are set on the object. + /// + private bool CheckFlagsAnd(Flags flags) + { + return ((_flags & flags) == flags); + } + + /// + /// CheckFlagsOr returns true if at least one flag in the + /// given bitmask is set. + /// + /// + /// If no bits are set in the given bitmask, the method returns + /// true. + /// + private bool CheckFlagsOr(Flags flags) + { + return (flags == 0 || (_flags & flags) != 0); + } + + private static void OnShowGridLinesPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + Grid grid = (Grid)d; + + if (grid.ExtData != null // trivial grid is 1 by 1. there is no grid lines anyway + && grid.ListenToNotifications) + { + grid.InvalidateVisual(); + } + + grid.SetFlags((bool)e.NewValue, Flags.ShowGridLinesPropertyValue); + } - protected override void OnMeasureInvalidated() + private static void OnCellAttachedPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { - base.OnMeasureInvalidated(); - _sharedSizeHost?.InvalidateMeasure(this); + Visual child = d as Visual; + + if (child != null) + { + Grid grid = child.GetVisualParent(); + if (grid != null + && grid.ExtData != null + && grid.ListenToNotifications) + { + grid.CellsStructureDirty = true; + } + } } - private SharedSizeScopeHost _sharedSizeHost; - /// - /// Defines the SharedSizeScopeHost private property. - /// The ampersands are used to make accessing the property via xaml inconvenient. + /// Helper for Comparer methods. /// - internal static readonly AttachedProperty s_sharedSizeScopeHostProperty = - AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost"); + /// + /// true if one or both of x and y are null, in which case result holds + /// the relative sort order. + /// + private static bool CompareNullRefs(object x, object y, out int result) + { + result = 2; - private ColumnDefinitions _columnDefinitions; + if (x == null) + { + if (y == null) + { + result = 0; + } + else + { + result = -1; + } + } + else + { + if (y == null) + { + result = 1; + } + } - private RowDefinitions _rowDefinitions; + return (result != 2); + } - static Grid() + /// + /// Private version returning array of column definitions. + /// + private IReadOnlyList DefinitionsU { - AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); - IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopeChanged); + get { return (ExtData.DefinitionsU); } } - public Grid() + /// + /// Private version returning array of row definitions. + /// + private IReadOnlyList DefinitionsV { - this.AttachedToVisualTree += Grid_AttachedToVisualTree; - this.DetachedFromVisualTree += Grid_DetachedFromVisualTree; + get { return (ExtData.DefinitionsV); } } /// - /// Gets or sets the columns definitions for the grid. + /// Helper accessor to layout time array of definitions. /// - public ColumnDefinitions ColumnDefinitions + private DefinitionBase[] TempDefinitions { get { - if (_columnDefinitions == null) + ExtendedData extData = ExtData; + int requiredLength = Math.Max(DefinitionsU.Count, DefinitionsV.Count) * 2; + + if (extData.TempDefinitions == null + || extData.TempDefinitions.Length < requiredLength) { - ColumnDefinitions = new ColumnDefinitions(); + WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(s_tempDefinitionsDataSlot); + if (tempDefinitionsWeakRef == null) + { + extData.TempDefinitions = new DefinitionBase[requiredLength]; + Thread.SetData(s_tempDefinitionsDataSlot, new WeakReference(extData.TempDefinitions)); + } + else + { + extData.TempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; + if (extData.TempDefinitions == null + || extData.TempDefinitions.Length < requiredLength) + { + extData.TempDefinitions = new DefinitionBase[requiredLength]; + tempDefinitionsWeakRef.Target = extData.TempDefinitions; + } + } } - - return _columnDefinitions; + return (extData.TempDefinitions); } + } - set + /// + /// Helper accessor to definition indices. + /// + private int[] DefinitionIndices + { + get { - if (_columnDefinitions != null) + int requiredLength = Math.Max(Math.Max(DefinitionsU.Count, DefinitionsV.Count), 1) * 2; + + if (_definitionIndices == null || _definitionIndices.Length < requiredLength) { - throw new NotSupportedException("Reassigning ColumnDefinitions not yet implemented."); + _definitionIndices = new int[requiredLength]; } - _columnDefinitions = value; - _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); + return _definitionIndices; } } /// - /// Gets or sets the row definitions for the grid. + /// Helper accessor to rounding errors. /// - public RowDefinitions RowDefinitions + private double[] RoundingErrors { get { - if (_rowDefinitions == null) + int requiredLength = Math.Max(DefinitionsU.Count, DefinitionsV.Count); + + if (_roundingErrors == null && requiredLength == 0) { - RowDefinitions = new RowDefinitions(); + _roundingErrors = new double[1]; } - - return _rowDefinitions; - } - - set - { - if (_rowDefinitions != null) + else if (_roundingErrors == null || _roundingErrors.Length < requiredLength) { - throw new NotSupportedException("Reassigning RowDefinitions not yet implemented."); + _roundingErrors = new double[requiredLength]; } - - _rowDefinitions = value; - _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); + return _roundingErrors; } } /// - /// Gets the value of the Column attached property for a control. + /// Private version returning array of cells. /// - /// The control. - /// The control's column. - public static int GetColumn(AvaloniaObject element) + private CellCache[] PrivateCells { - return element.GetValue(ColumnProperty); + get { return (ExtData.CellCachesCollection); } } /// - /// Gets the value of the ColumnSpan attached property for a control. + /// Convenience accessor to ValidCellsStructure bit flag. /// - /// The control. - /// The control's column span. - public static int GetColumnSpan(AvaloniaObject element) + private bool CellsStructureDirty { - return element.GetValue(ColumnSpanProperty); + get { return (!CheckFlagsAnd(Flags.ValidCellsStructure)); } + set { SetFlags(!value, Flags.ValidCellsStructure); } } /// - /// Gets the value of the Row attached property for a control. + /// Convenience accessor to ListenToNotifications bit flag. /// - /// The control. - /// The control's row. - public static int GetRow(AvaloniaObject element) + private bool ListenToNotifications { - return element.GetValue(RowProperty); + get { return (CheckFlagsAnd(Flags.ListenToNotifications)); } + set { SetFlags(value, Flags.ListenToNotifications); } } /// - /// Gets the value of the RowSpan attached property for a control. + /// Convenience accessor to SizeToContentU bit flag. /// - /// The control. - /// The control's row span. - public static int GetRowSpan(AvaloniaObject element) + private bool SizeToContentU { - return element.GetValue(RowSpanProperty); + get { return (CheckFlagsAnd(Flags.SizeToContentU)); } + set { SetFlags(value, Flags.SizeToContentU); } } + /// + /// Convenience accessor to SizeToContentV bit flag. + /// + private bool SizeToContentV + { + get { return (CheckFlagsAnd(Flags.SizeToContentV)); } + set { SetFlags(value, Flags.SizeToContentV); } + } /// - /// Gets the value of the IsSharedSizeScope attached property for a control. + /// Convenience accessor to HasStarCellsU bit flag. /// - /// The control. - /// The control's IsSharedSizeScope value. - public static bool GetIsSharedSizeScope(AvaloniaObject element) + private bool HasStarCellsU { - return element.GetValue(IsSharedSizeScopeProperty); + get { return (CheckFlagsAnd(Flags.HasStarCellsU)); } + set { SetFlags(value, Flags.HasStarCellsU); } } /// - /// Sets the value of the Column attached property for a control. + /// Convenience accessor to HasStarCellsV bit flag. /// - /// The control. - /// The column value. - public static void SetColumn(AvaloniaObject element, int value) + private bool HasStarCellsV { - element.SetValue(ColumnProperty, value); + get { return (CheckFlagsAnd(Flags.HasStarCellsV)); } + set { SetFlags(value, Flags.HasStarCellsV); } } /// - /// Sets the value of the ColumnSpan attached property for a control. + /// Convenience accessor to HasGroup3CellsInAutoRows bit flag. /// - /// The control. - /// The column span value. - public static void SetColumnSpan(AvaloniaObject element, int value) + private bool HasGroup3CellsInAutoRows { - element.SetValue(ColumnSpanProperty, value); + get { return (CheckFlagsAnd(Flags.HasGroup3CellsInAutoRows)); } + set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); } } /// - /// Sets the value of the Row attached property for a control. + /// fp version of d == 0. /// - /// The control. - /// The row value. - public static void SetRow(AvaloniaObject element, int value) + /// Value to check. + /// true if d == 0. + private static bool _IsZero(double d) { - element.SetValue(RowProperty, value); + return (Math.Abs(d) < double.Epsilon); } /// - /// Sets the value of the RowSpan attached property for a control. + /// fp version of d1 == d2 /// - /// The control. - /// The row span value. - public static void SetRowSpan(AvaloniaObject element, int value) + /// First value to compare + /// Second value to compare + /// true if d1 == d2 + private static bool _AreClose(double d1, double d2) { - element.SetValue(RowSpanProperty, value); + return (Math.Abs(d1 - d2) < double.Epsilon); } /// - /// Sets the value of IsSharedSizeScope property for a control. + /// Returns reference to extended data bag. /// - /// The control. - /// The IsSharedSizeScope value. - public static void SetIsSharedSizeScope(AvaloniaObject element, bool value) + private ExtendedData ExtData { - element.SetValue(IsSharedSizeScopeProperty, value); + get { return (_data); } + } + + /// + /// Returns *-weight, adjusted for scale computed during Phase 1 + /// + static double StarWeight(DefinitionBase def, double scale) + { + if (scale < 0.0) + { + // if one of the *-weights is Infinity, adjust the weights by mapping + // Infinty to 1.0 and everything else to 0.0: the infinite items share the + // available space equally, everyone else gets nothing. + return (Double.IsPositiveInfinity(def.UserSize.Value)) ? 1.0 : 0.0; + } + else + { + return def.UserSize.Value * scale; + } + } + + // Extended data instantiated on demand, for non-trivial case handling only + private ExtendedData _data; + + // Grid validity / property caches dirtiness flags + private Flags _flags; + private GridLinesRenderer _gridLinesRenderer; + + // Keeps track of definition indices. + int[] _definitionIndices; + + // Stores unrounded values and rounding errors during layout rounding. + double[] _roundingErrors; + + // 5 is an arbitrary constant chosen to end the measure loop + private const int c_layoutLoopMaxCount = 5; + + private static readonly LocalDataStoreSlot s_tempDefinitionsDataSlot = Thread.AllocateDataSlot(); + private static readonly IComparer s_spanPreferredDistributionOrderComparer = new SpanPreferredDistributionOrderComparer(); + private static readonly IComparer s_spanMaxDistributionOrderComparer = new SpanMaxDistributionOrderComparer(); + private static readonly IComparer s_minRatioComparer = new MinRatioComparer(); + private static readonly IComparer s_maxRatioComparer = new MaxRatioComparer(); + private static readonly IComparer s_starWeightComparer = new StarWeightComparer(); + + /// + /// Extended data instantiated on demand, when grid handles non-trivial case. + /// + private class ExtendedData + { + internal ColumnDefinitions ColumnDefinitions; // collection of column definitions (logical tree support) + internal RowDefinitions RowDefinitions; // collection of row definitions (logical tree support) + internal IReadOnlyList DefinitionsU; // collection of column definitions used during calc + internal IReadOnlyList DefinitionsV; // collection of row definitions used during calc + internal CellCache[] CellCachesCollection; // backing store for logical children + internal int CellGroup1; // index of the first cell in first cell group + internal int CellGroup2; // index of the first cell in second cell group + internal int CellGroup3; // index of the first cell in third cell group + internal int CellGroup4; // index of the first cell in forth cell group + internal DefinitionBase[] TempDefinitions; // temporary array used during layout for various purposes + // TempDefinitions.Length == Max(definitionsU.Length, definitionsV.Length) } /// - /// Gets the result of the last column measurement. - /// Use this result to reduce the arrange calculation. + /// Grid validity / property caches dirtiness flags /// - private GridLayout.MeasureResult _columnMeasureCache; + [System.Flags] + private enum Flags + { + // + // the foolowing flags let grid tracking dirtiness in more granular manner: + // * Valid???Structure flags indicate that elements were added or removed. + // * Valid???Layout flags indicate that layout time portion of the information + // stored on the objects should be updated. + // + ValidDefinitionsUStructure = 0x00000001, + ValidDefinitionsVStructure = 0x00000002, + ValidCellsStructure = 0x00000004, + + // + // boolean properties state + // + ShowGridLinesPropertyValue = 0x00000100, // show grid lines ? + + // + // boolean flags + // + ListenToNotifications = 0x00001000, // "0" when all notifications are ignored + SizeToContentU = 0x00002000, // "1" if calculating to content in U direction + SizeToContentV = 0x00004000, // "1" if calculating to content in V direction + HasStarCellsU = 0x00008000, // "1" if at least one cell belongs to a Star column + HasStarCellsV = 0x00010000, // "1" if at least one cell belongs to a Star row + HasGroup3CellsInAutoRows = 0x00020000, // "1" if at least one cell of group 3 belongs to an Auto row + MeasureOverrideInProgress = 0x00040000, // "1" while in the context of Grid.MeasureOverride + ArrangeOverrideInProgress = 0x00080000, // "1" while in the context of Grid.ArrangeOverride + } /// - /// Gets the result of the last row measurement. - /// Use this result to reduce the arrange calculation. + /// ShowGridLines property. This property is used mostly + /// for simplification of visual debuggig. When it is set + /// to true grid lines are drawn to visualize location + /// of grid lines. /// - private GridLayout.MeasureResult _rowMeasureCache; + public static readonly StyledProperty ShowGridLinesProperty = + AvaloniaProperty.Register(nameof(ShowGridLines)); /// - /// Gets the row layout as of the last measure. + /// Column property. This is an attached property. + /// Grid defines Column property, so that it can be set + /// on any element treated as a cell. Column property + /// specifies child's position with respect to columns. /// - private GridLayout _rowLayoutCache; + /// + /// Columns are 0 - based. In order to appear in first column, element + /// should have Column property set to 0. + /// Default value for the property is 0. + /// + public static readonly AttachedProperty ColumnProperty = + AvaloniaProperty.RegisterAttached( + "Column", + defaultValue: 0, + validate: (_, v) => + { + if (v >= 0) return v; + else throw new ArgumentException("Invalid Grid.Column value."); + }); /// - /// Gets the column layout as of the last measure. + /// Row property. This is an attached property. + /// Grid defines Row, so that it can be set + /// on any element treated as a cell. Row property + /// specifies child's position with respect to rows. + /// + /// Rows are 0 - based. In order to appear in first row, element + /// should have Row property set to 0. + /// Default value for the property is 0. + /// /// - private GridLayout _columnLayoutCache; + public static readonly AttachedProperty RowProperty = + AvaloniaProperty.RegisterAttached( + "Row", + defaultValue: 0, + validate: (_, v) => + { + if (v >= 0) return v; + else throw new ArgumentException("Invalid Grid.Row value."); + }); /// - /// Measures the grid. + /// ColumnSpan property. This is an attached property. + /// Grid defines ColumnSpan, so that it can be set + /// on any element treated as a cell. ColumnSpan property + /// specifies child's width with respect to columns. + /// Example, ColumnSpan == 2 means that child will span across two columns. /// - /// The available size. - /// The desired size of the control. - protected override Size MeasureOverride(Size constraint) - { - // Situation 1/2: - // If the grid doesn't have any column/row definitions, it behaves like a normal panel. - // GridLayout supports this situation but we handle this separately for performance. + /// + /// Default value for the property is 1. + /// + public static readonly AttachedProperty ColumnSpanProperty = + AvaloniaProperty.RegisterAttached( + "ColumnSpan", + defaultValue: 1, + validate: (_, v) => + { + if (v >= 1) return v; + else throw new ArgumentException("Invalid Grid.ColumnSpan value."); + }); - if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) - { - var maxWidth = 0.0; - var maxHeight = 0.0; - foreach (var child in Children.OfType()) + /// + /// RowSpan property. This is an attached property. + /// Grid defines RowSpan, so that it can be set + /// on any element treated as a cell. RowSpan property + /// specifies child's height with respect to row grid lines. + /// Example, RowSpan == 3 means that child will span across three rows. + /// + /// + /// Default value for the property is 1. + /// + public static readonly AttachedProperty RowSpanProperty = + AvaloniaProperty.RegisterAttached( + "RowSpan", + defaultValue: 1, + validate: (_, v) => { - child.Measure(constraint); - maxWidth = Math.Max(maxWidth, child.DesiredSize.Width); - maxHeight = Math.Max(maxHeight, child.DesiredSize.Height); - } + if (v >= 1) return v; + else throw new ArgumentException("Invalid Grid.RowSpan value."); + }); - maxWidth = Math.Min(maxWidth, constraint.Width); - maxHeight = Math.Min(maxHeight, constraint.Height); - return new Size(maxWidth, maxHeight); - } + /// + /// IsSharedSizeScope property marks scoping element for shared size. + /// + public static readonly AttachedProperty IsSharedSizeScopeProperty = + AvaloniaProperty.RegisterAttached( + "IsSharedSizeScope"); - // Situation 2/2: - // If the grid defines some columns or rows. - // Debug Tip: - // - GridLayout doesn't hold any state, so you can drag the debugger execution - // arrow back to any statements and re-run them without any side-effect. + /// + /// LayoutTimeSizeType is used internally and reflects layout-time size type. + /// + [System.Flags] + internal enum LayoutTimeSizeType : byte + { + None = 0x00, + Pixel = 0x01, + Auto = 0x02, + Star = 0x04, + } - var measureCache = new Dictionary(); - var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = new GridLayout(ColumnDefinitions); - var rowLayout = new GridLayout(RowDefinitions); - // Note: If a child stays in a * or Auto column/row, use constraint to measure it. - columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width); - rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height); + /// + /// CellCache stored calculated values of + /// 1. attached cell positioning properties; + /// 2. size type; + /// 3. index of a next cell in the group; + /// + private struct CellCache + { + internal int ColumnIndex; + internal int RowIndex; + internal int ColumnSpan; + internal int RowSpan; + internal LayoutTimeSizeType SizeTypeU; + internal LayoutTimeSizeType SizeTypeV; + internal int Next; + internal bool IsStarU { get { return ((SizeTypeU & LayoutTimeSizeType.Star) != 0); } } + internal bool IsAutoU { get { return ((SizeTypeU & LayoutTimeSizeType.Auto) != 0); } } + internal bool IsStarV { get { return ((SizeTypeV & LayoutTimeSizeType.Star) != 0); } } + internal bool IsAutoV { get { return ((SizeTypeV & LayoutTimeSizeType.Auto) != 0); } } + } - // Calculate measurement. - var columnResult = columnLayout.Measure(constraint.Width); - var rowResult = rowLayout.Measure(constraint.Height); + /// + /// Helper class for representing a key for a span in hashtable. + /// + private class SpanKey + { + /// + /// Constructor. + /// + /// Starting index of the span. + /// Span count. + /// true for columns; false for rows. + internal SpanKey(int start, int count, bool u) + { + _start = start; + _count = count; + _u = u; + } - // Use the results of the measurement to measure the rest of the children. - foreach (var child in Children.OfType()) + /// + /// + /// + public override int GetHashCode() { - var (column, columnSpan) = safeColumns[child]; - var (row, rowSpan) = safeRows[child]; - var width = Enumerable.Range(column, columnSpan).Select(x => columnResult.LengthList[x]).Sum(); - var height = Enumerable.Range(row, rowSpan).Select(x => rowResult.LengthList[x]).Sum(); + int hash = (_start ^ (_count << 2)); - MeasureOnce(child, new Size(width, height)); - } + if (_u) hash &= 0x7ffffff; + else hash |= 0x8000000; - // Cache the measure result and return the desired size. - _columnMeasureCache = columnResult; - _rowMeasureCache = rowResult; - _rowLayoutCache = rowLayout; - _columnLayoutCache = columnLayout; + return (hash); + } - if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) + /// + /// + /// + public override bool Equals(object obj) { - _sharedSizeHost.UpdateMeasureStatus(this, rowResult, columnResult); + SpanKey sk = obj as SpanKey; + return (sk != null + && sk._start == _start + && sk._count == _count + && sk._u == _u); } - return new Size(columnResult.DesiredLength, rowResult.DesiredLength); + /// + /// Returns start index of the span. + /// + internal int Start { get { return (_start); } } + + /// + /// Returns span count. + /// + internal int Count { get { return (_count); } } + + /// + /// Returns true if this is a column span. + /// false if this is a row span. + /// + internal bool U { get { return (_u); } } + + private int _start; + private int _count; + private bool _u; + } - // Measure each child only once. - // If a child has been measured, it will just return the desired size. - Size MeasureOnce(Control child, Size size) + /// + /// SpanPreferredDistributionOrderComparer. + /// + private class SpanPreferredDistributionOrderComparer : IComparer + { + public int Compare(object x, object y) { - if (measureCache.TryGetValue(child, out var desiredSize)) + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) { - return desiredSize; + if (definitionX.UserSize.IsAuto) + { + if (definitionY.UserSize.IsAuto) + { + result = definitionX.MinSize.CompareTo(definitionY.MinSize); + } + else + { + result = -1; + } + } + else + { + if (definitionY.UserSize.IsAuto) + { + result = +1; + } + else + { + result = definitionX.PreferredSize.CompareTo(definitionY.PreferredSize); + } + } } - child.Measure(size); - desiredSize = child.DesiredSize; - measureCache[child] = desiredSize; - return desiredSize; + return result; } } /// - /// Arranges the grid's children. + /// SpanMaxDistributionOrderComparer. /// - /// The size allocated to the control. - /// The space taken. - protected override Size ArrangeOverride(Size finalSize) + private class SpanMaxDistributionOrderComparer : IComparer { - // Situation 1/2: - // If the grid doesn't have any column/row definitions, it behaves like a normal panel. - // GridLayout supports this situation but we handle this separately for performance. - - if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) + public int Compare(object x, object y) { - foreach (var child in Children.OfType()) + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) { - child.Arrange(new Rect(finalSize)); + if (definitionX.UserSize.IsAuto) + { + if (definitionY.UserSize.IsAuto) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + else + { + result = +1; + } + } + else + { + if (definitionY.UserSize.IsAuto) + { + result = -1; + } + else + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + } } - return finalSize; + return result; } + } - // Situation 2/2: - // If the grid defines some columns or rows. - // Debug Tip: - // - GridLayout doesn't hold any state, so you can drag the debugger execution - // arrow back to any statements and re-run them without any side-effect. - - var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = _columnLayoutCache; - var rowLayout = _rowLayoutCache; - - var rowCache = _rowMeasureCache; - var columnCache = _columnMeasureCache; + /// + /// StarDistributionOrderIndexComparer. + /// + private class StarDistributionOrderIndexComparer : IComparer + { + private readonly IReadOnlyList definitions; - if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) + internal StarDistributionOrderIndexComparer(IReadOnlyList definitions) { - (rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache); - - rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList); - columnCache = columnLayout.Measure(finalSize.Width, columnCache.LeanLengthList); + Contract.Requires(definitions != null); + this.definitions = definitions; } - // Calculate for arrange result. - var columnResult = columnLayout.Arrange(finalSize.Width, columnCache); - var rowResult = rowLayout.Arrange(finalSize.Height, rowCache); - // Arrange the children. - foreach (var child in Children.OfType()) + public int Compare(object x, object y) { - var (column, columnSpan) = safeColumns[child]; - var (row, rowSpan) = safeRows[child]; - var x = Enumerable.Range(0, column).Sum(c => columnResult.LengthList[c]); - var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]); - var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]); - var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]); - child.Arrange(new Rect(x, y, width, height)); - } + int? indexX = x as int?; + int? indexY = y as int?; - // Assign the actual width. - for (var i = 0; i < ColumnDefinitions.Count; i++) - { - ColumnDefinitions[i].ActualWidth = columnResult.LengthList[i]; - } + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; - // Assign the actual height. - for (var i = 0; i < RowDefinitions.Count; i++) - { - RowDefinitions[i].ActualHeight = rowResult.LengthList[i]; - } + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } - // Return the render size. - return finalSize; + return result; + } } /// - /// Tests whether this grid belongs to a shared size scope. + /// DistributionOrderComparer. /// - /// True if the grid is registered in a shared size scope. - internal bool HasSharedSizeScope() + private class DistributionOrderIndexComparer : IComparer { - return _sharedSizeHost != null; + private readonly IReadOnlyList definitions; + + internal DistributionOrderIndexComparer(IReadOnlyList definitions) + { + Contract.Requires(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + double xprime = definitionX.SizeCache - definitionX.MinSizeForArrange; + double yprime = definitionY.SizeCache - definitionY.MinSizeForArrange; + result = xprime.CompareTo(yprime); + } + + return result; + } } /// - /// Called when the SharedSizeScope for a given grid has changed. - /// Unregisters the grid from it's current scope and finds a new one (if any) + /// RoundingErrorIndexComparer. /// - /// - /// This method, while not efficient, correctly handles nested scopes, with any order of scope changes. - /// - internal void SharedScopeChanged() + private class RoundingErrorIndexComparer : IComparer { - _sharedSizeHost?.UnegisterGrid(this); - - _sharedSizeHost = null; - var scope = this.GetVisualAncestors().OfType() - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + private readonly double[] errors; - if (scope != null) + internal RoundingErrorIndexComparer(double[] errors) { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); + Contract.Requires(errors != null); + this.errors = errors; } - InvalidateMeasure(); + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + int result; + + if (!CompareNullRefs(indexX, indexY, out result)) + { + double errorX = errors[indexX.Value]; + double errorY = errors[indexY.Value]; + result = errorX.CompareTo(errorY); + } + + return result; + } } /// - /// Callback when a grid is attached to the visual tree. Finds the innermost SharedSizeScope and registers the grid - /// in it. + /// MinRatioComparer. + /// Sort by w/min (stored in MeasureSize), descending. + /// We query the list from the back, i.e. in ascending order of w/min. /// - /// The source of the event. - /// The event arguments. - private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) + private class MinRatioComparer : IComparer { - var scope = - new Control[] { this }.Concat(this.GetVisualAncestors().OfType()) - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; - if (_sharedSizeHost != null) - throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!"); + int result; - if (scope != null) - { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); + if (!CompareNullRefs(definitionY, definitionX, out result)) + { + result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); + } + + return result; } } /// - /// Callback when a grid is detached from the visual tree. Unregisters the grid from its SharedSizeScope if any. + /// MaxRatioComparer. + /// Sort by w/max (stored in SizeCache), ascending. + /// We query the list from the back, i.e. in descending order of w/max. /// - /// The source of the event. - /// The event arguments. - private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) + private class MaxRatioComparer : IComparer { - _sharedSizeHost?.UnegisterGrid(this); - _sharedSizeHost = null; - } + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + return result; + } + } /// - /// Get the safe column/columnspan and safe row/rowspan. - /// This method ensures that none of the children has a column/row outside the bounds of the definitions. + /// StarWeightComparer. + /// Sort by *-weight (stored in MeasureSize), ascending. /// - [Pure] - private (Dictionary safeColumns, - Dictionary safeRows) GetSafeColumnRows() + private class StarWeightComparer : IComparer { - var columnCount = ColumnDefinitions.Count; - var rowCount = RowDefinitions.Count; - columnCount = columnCount == 0 ? 1 : columnCount; - rowCount = rowCount == 0 ? 1 : rowCount; - var safeColumns = Children.OfType().ToDictionary(child => child, - child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child))); - var safeRows = Children.OfType().ToDictionary(child => child, - child => GetSafeSpan(rowCount, GetRow(child), GetRowSpan(child))); - return (safeColumns, safeRows); + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); + } + + return result; + } } /// - /// Gets the safe row/column and rowspan/columnspan for a specified range. - /// The user may assign row/column properties outside the bounds of the row/column count, this method coerces them inside. + /// MinRatioIndexComparer. /// - /// The row or column count. - /// The row or column that the user assigned. - /// The rowspan or columnspan that the user assigned. - /// The safe row/column and rowspan/columnspan. - [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (int index, int span) GetSafeSpan(int length, int userIndex, int userSpan) + private class MinRatioIndexComparer : IComparer { - var index = userIndex; - var span = userSpan; + private readonly IReadOnlyList definitions; - if (index < 0) + internal MinRatioIndexComparer(IReadOnlyList definitions) { - span = index + span; - index = 0; + Contract.Requires(definitions != null); + this.definitions = definitions; } - if (span <= 0) + public int Compare(object x, object y) { - span = 1; - } + int? indexX = x as int?; + int? indexY = y as int?; - if (userIndex >= length) - { - index = length - 1; - span = 1; - } - else if (userIndex + userSpan > length) - { - span = length - userIndex; - } + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } - return (index, span); + int result; + + if (!CompareNullRefs(definitionY, definitionX, out result)) + { + result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); + } + + return result; + } } - private static int ValidateColumn(AvaloniaObject o, int value) + /// + /// MaxRatioIndexComparer. + /// + private class MaxRatioIndexComparer : IComparer { - if (value < 0) + private readonly IReadOnlyList definitions; + + internal MaxRatioIndexComparer(IReadOnlyList definitions) { - throw new ArgumentException("Invalid Grid.Column value."); + Contract.Requires(definitions != null); + this.definitions = definitions; } - return value; + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + + return result; + } } - private static int ValidateRow(AvaloniaObject o, int value) + /// + /// MaxRatioIndexComparer. + /// + private class StarWeightIndexComparer : IComparer { - if (value < 0) + private readonly IReadOnlyList definitions; + + internal StarWeightIndexComparer(IReadOnlyList definitions) { - throw new ArgumentException("Invalid Grid.Row value."); + Contract.Requires(definitions != null); + this.definitions = definitions; } - return value; + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); + } + + return result; + } } /// - /// Called when the value of changes for a control. + /// Helper for rendering grid lines. /// - /// The control that triggered the change. - /// Change arguments. - private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) + internal class GridLinesRenderer : Control { - var shouldDispose = (arg2.OldValue is bool d) && d; - if (shouldDispose) + /// + /// Static initialization + /// + static GridLinesRenderer() { - var host = source.GetValue(s_sharedSizeScopeHostProperty) as SharedSizeScopeHost; - if (host == null) - throw new AvaloniaInternalException("SharedScopeHost wasn't set when IsSharedSizeScope was true!"); - host.Dispose(); - source.ClearValue(s_sharedSizeScopeHostProperty); + var dashArray = new List() { _dashLength, _dashLength }; + + var ds1 = new DashStyle(dashArray, 0); + _oddDashPen = new Pen(Brushes.Blue, + _penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds1); + + var ds2 = new DashStyle(dashArray, _dashLength); + _evenDashPen = new Pen(Brushes.Yellow, + _penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds2); } - var shouldAssign = (arg2.NewValue is bool a) && a; - if (shouldAssign) + /// + /// UpdateRenderBounds. + /// + public override void Render(DrawingContext drawingContext) { - if (source.GetValue(s_sharedSizeScopeHostProperty) != null) - throw new AvaloniaInternalException("SharedScopeHost was already set when IsSharedSizeScope is only now being set to true!"); - source.SetValue(s_sharedSizeScopeHostProperty, new SharedSizeScopeHost()); + var grid = this.GetVisualParent(); + + if (grid == null || !grid.ShowGridLines) + return; + + for (int i = 1; i < grid.ColumnDefinitions.Count; ++i) + { + DrawGridLine( + drawingContext, + grid.ColumnDefinitions[i].FinalOffset, 0.0, + grid.ColumnDefinitions[i].FinalOffset, _lastArrangeSize.Height); + } + + for (int i = 1; i < grid.RowDefinitions.Count; ++i) + { + DrawGridLine( + drawingContext, + 0.0, grid.RowDefinitions[i].FinalOffset, + _lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset); + } } - // if the scope has changed, notify the descendant grids that they need to update. - if (source.GetVisualRoot() != null && shouldAssign || shouldDispose) + /// + /// Draw single hi-contrast line. + /// + private static void DrawGridLine( + DrawingContext drawingContext, + double startX, + double startY, + double endX, + double endY) { - var participatingGrids = new[] { source }.Concat(source.GetVisualDescendants()).OfType(); - - foreach (var grid in participatingGrids) - grid.SharedScopeChanged(); + var start = new Point(startX, startY); + var end = new Point(endX, endY); + drawingContext.DrawLine(_oddDashPen, start, end); + drawingContext.DrawLine(_evenDashPen, start, end); + } + internal void UpdateRenderBounds(Size arrangeSize) + { + _lastArrangeSize = arrangeSize; + this.InvalidateVisual(); } + + private static Size _lastArrangeSize; + private const double _dashLength = 4.0; // + private const double _penWidth = 1.0; // + private static readonly Pen _oddDashPen; // first pen to draw dash + private static readonly Pen _evenDashPen; // second pen to draw dash } } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index d8473dc613..bd558af5ef 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -20,8 +21,6 @@ namespace Avalonia.Controls /// public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable { - private ICommand _command; - /// /// Defines the property. /// @@ -91,9 +90,8 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); - /// - /// The submenu popup. - /// + private ICommand _command; + private bool _commandCanExecute = true; private Popup _popup; /// @@ -231,6 +229,8 @@ namespace Avalonia.Controls /// IMenuElement IMenuItem.Parent => Parent as IMenuElement; + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + /// bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); @@ -337,12 +337,9 @@ namespace Avalonia.Controls { base.OnPointerEnter(e); - RaiseEvent(new PointerEventArgs - { - Device = e.Device, - RoutedEvent = PointerEnterItemEvent, - Source = this, - }); + var point = e.GetPointerPoint(null); + RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position, + e.Timestamp, point.Properties, e.InputModifiers)); } /// @@ -350,12 +347,9 @@ namespace Avalonia.Controls { base.OnPointerLeave(e); - RaiseEvent(new PointerEventArgs - { - Device = e.Device, - RoutedEvent = PointerLeaveItemEvent, - Source = this, - }); + var point = e.GetPointerPoint(null); + RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position, + e.Timestamp, point.Properties, e.InputModifiers)); } /// @@ -400,6 +394,22 @@ namespace Avalonia.Controls } } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + { + base.UpdateDataValidation(property, status); + if (property == CommandProperty) + { + if (status?.ErrorType == BindingErrorType.Error) + { + if (_commandCanExecute) + { + _commandCanExecute = false; + UpdateIsEffectivelyEnabled(); + } + } + } + } + /// /// Closes all submenus of the menu item. /// @@ -443,9 +453,13 @@ namespace Avalonia.Controls /// The event args. private void CanExecuteChanged(object sender, EventArgs e) { - // HACK: Just set the IsEnabled property for the moment. This needs to be changed to - // use IsEnabledCore etc. but it will do for now. - IsEnabled = Command == null || Command.CanExecute(CommandParameter); + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } } /// diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 73854b9f60..5f63a44717 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -373,9 +373,9 @@ namespace Avalonia.Controls.Platform protected internal virtual void RawInput(RawInputEventArgs e) { - var mouse = e as RawMouseEventArgs; + var mouse = e as RawPointerEventArgs; - if (mouse?.Type == RawMouseEventType.NonClientLeftButtonDown) + if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) { Menu.Close(); } diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs index 0918da1a90..76f17332bf 100644 --- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs +++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs @@ -43,7 +43,7 @@ namespace Avalonia.Platform _lastPosition = default(Point); _allowedEffects = allowedEffects; - using (_inputManager.PreProcess.OfType().Subscribe(ProcessMouseEvents)) + using (_inputManager.PreProcess.OfType().Subscribe(ProcessMouseEvents)) { using (_inputManager.PreProcess.OfType().Subscribe(ProcessKeyEvents)) { @@ -153,7 +153,7 @@ namespace Avalonia.Platform } } - private void ProcessMouseEvents(RawMouseEventArgs e) + private void ProcessMouseEvents(RawPointerEventArgs e) { if (!_initialInputModifiers.HasValue) _initialInputModifiers = e.InputModifiers & MOUSE_INPUTMODIFIERS; @@ -174,22 +174,22 @@ namespace Avalonia.Platform switch (e.Type) { - case RawMouseEventType.LeftButtonDown: - case RawMouseEventType.RightButtonDown: - case RawMouseEventType.MiddleButtonDown: - case RawMouseEventType.NonClientLeftButtonDown: + case RawPointerEventType.LeftButtonDown: + case RawPointerEventType.RightButtonDown: + case RawPointerEventType.MiddleButtonDown: + case RawPointerEventType.NonClientLeftButtonDown: CancelDragging(); e.Handled = true; return; - case RawMouseEventType.LeaveWindow: + case RawPointerEventType.LeaveWindow: RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, e.Root, e.Position, e.InputModifiers); break; - case RawMouseEventType.LeftButtonUp: + case RawPointerEventType.LeftButtonUp: CheckDraggingAccepted(InputModifiers.LeftMouseButton); break; - case RawMouseEventType.MiddleButtonUp: + case RawPointerEventType.MiddleButtonUp: CheckDraggingAccepted(InputModifiers.MiddleMouseButton); break; - case RawMouseEventType.RightButtonUp: + case RawPointerEventType.RightButtonUp: CheckDraggingAccepted(InputModifiers.RightMouseButton); break; - case RawMouseEventType.Move: + case RawPointerEventType.Move: var mods = e.InputModifiers & MOUSE_INPUTMODIFIERS; if (_initialInputModifiers.Value != mods) { diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 30330ef9ac..e7d8018a42 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.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.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -64,6 +65,7 @@ namespace Avalonia.Controls.Presenters private Vector _offset; private IDisposable _logicalScrollSubscription; private Size _viewport; + private Dictionary _activeLogicalGestureScrolls; /// /// Initializes static members of the class. @@ -81,6 +83,7 @@ namespace Avalonia.Controls.Presenters public ScrollContentPresenter() { AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); + AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture); this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } @@ -227,6 +230,72 @@ namespace Avalonia.Controls.Presenters return finalSize; } + // Arbitrary chosen value, probably need to ask ILogicalScrollable + private const int LogicalScrollItemSize = 50; + private void OnScrollGesture(object sender, ScrollGestureEventArgs e) + { + if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width) + { + var scrollable = Child as ILogicalScrollable; + bool isLogical = scrollable?.IsLogicalScrollEnabled == true; + + double x = Offset.X; + double y = Offset.Y; + + Vector delta = default; + if (isLogical) + _activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta); + delta += e.Delta; + + if (Extent.Height > Viewport.Height) + { + double dy; + if (isLogical) + { + var logicalUnits = delta.Y / LogicalScrollItemSize; + delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize); + dy = logicalUnits * scrollable.ScrollSize.Height; + } + else + dy = delta.Y; + + + y += dy; + y = Math.Max(y, 0); + y = Math.Min(y, Extent.Height - Viewport.Height); + } + + if (Extent.Width > Viewport.Width) + { + double dx; + if (isLogical) + { + var logicalUnits = delta.X / LogicalScrollItemSize; + delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize); + dx = logicalUnits * scrollable.ScrollSize.Width; + } + else + dx = delta.X; + x += dx; + x = Math.Max(x, 0); + x = Math.Min(x, Extent.Width - Viewport.Width); + } + + if (isLogical) + { + if (_activeLogicalGestureScrolls == null) + _activeLogicalGestureScrolls = new Dictionary(); + _activeLogicalGestureScrolls[e.Id] = delta; + } + + Offset = new Vector(x, y); + e.Handled = true; + } + } + + private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e) + => _activeLogicalGestureScrolls?.Remove(e.Id); + /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index fbdf885709..b3345ec101 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -19,6 +19,15 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); + public static readonly StyledProperty SelectionBrushProperty = + AvaloniaProperty.Register(nameof(SelectionBrushProperty)); + + public static readonly StyledProperty SelectionForegroundBrushProperty = + AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); + + public static readonly StyledProperty CaretBrushProperty = + AvaloniaProperty.Register(nameof(CaretBrushProperty)); + public static readonly DirectProperty SelectionStartProperty = TextBox.SelectionStartProperty.AddOwner( o => o.SelectionStart, @@ -34,11 +43,12 @@ namespace Avalonia.Controls.Presenters private int _selectionStart; private int _selectionEnd; private bool _caretBlink; - private IBrush _highlightBrush; - + static TextPresenter() { - AffectsRender(PasswordCharProperty); + AffectsRender(PasswordCharProperty, + SelectionBrushProperty, SelectionForegroundBrushProperty, + SelectionStartProperty, SelectionEndProperty); } public TextPresenter() @@ -79,6 +89,24 @@ namespace Avalonia.Controls.Presenters set => SetValue(PasswordCharProperty, value); } + public IBrush SelectionBrush + { + get => GetValue(SelectionBrushProperty); + set => SetValue(SelectionBrushProperty, value); + } + + public IBrush SelectionForegroundBrush + { + get => GetValue(SelectionForegroundBrushProperty); + set => SetValue(SelectionForegroundBrushProperty, value); + } + + public IBrush CaretBrush + { + get => GetValue(CaretBrushProperty); + set => SetValue(CaretBrushProperty, value); + } + public int SelectionStart { get @@ -129,14 +157,9 @@ namespace Avalonia.Controls.Presenters var rects = FormattedText.HitTestTextRange(start, length); - if (_highlightBrush == null) - { - _highlightBrush = (IBrush)this.FindResource("HighlightBrush"); - } - foreach (var rect in rects) { - context.FillRectangle(_highlightBrush, rect); + context.FillRectangle(SelectionBrush, rect); } } @@ -144,16 +167,21 @@ namespace Avalonia.Controls.Presenters if (selectionStart == selectionEnd) { - var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color; - var caretBrush = Brushes.Black; + var caretBrush = CaretBrush; - if (backgroundColor.HasValue) + if (caretBrush is null) { - byte red = (byte)~(backgroundColor.Value.R); - byte green = (byte)~(backgroundColor.Value.G); - byte blue = (byte)~(backgroundColor.Value.B); - - caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue)); + var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color; + if (backgroundColor.HasValue) + { + byte red = (byte)~(backgroundColor.Value.R); + byte green = (byte)~(backgroundColor.Value.G); + byte blue = (byte)~(backgroundColor.Value.B); + + caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue)); + } + else + caretBrush = Brushes.Black; } if (_caretBlink) @@ -252,7 +280,7 @@ namespace Avalonia.Controls.Presenters { result.Spans = new[] { - new FormattedTextStyleSpan(start, length, foregroundBrush: Brushes.White), + new FormattedTextStyleSpan(start, length, SelectionForegroundBrush), }; } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index f349bcf059..e02d46c1df 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -421,9 +421,9 @@ namespace Avalonia.Controls.Primitives private void ListenForNonClientClick(RawInputEventArgs e) { - var mouse = e as RawMouseEventArgs; + var mouse = e as RawPointerEventArgs; - if (!StaysOpen && mouse?.Type == RawMouseEventType.NonClientLeftButtonDown) + if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) { Close(); } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 280c3ad93a..7b91d6235d 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -54,7 +54,8 @@ namespace Avalonia.Controls.Primitives nameof(SelectedIndex), o => o.SelectedIndex, (o, v) => o.SelectedIndex = v, - unsetValue: -1); + unsetValue: -1, + defaultBindingMode: BindingMode.TwoWay); /// /// Defines the property. @@ -380,6 +381,7 @@ namespace Avalonia.Controls.Primitives } break; + case NotifyCollectionChangedAction.Move: case NotifyCollectionChangedAction.Reset: SelectedIndex = IndexOf(Items, SelectedItem); break; @@ -644,20 +646,20 @@ namespace Avalonia.Controls.Primitives /// The desired items. internal static void SynchronizeItems(IList items, IEnumerable desired) { - int index = 0; + var index = 0; - foreach (var i in desired) + foreach (object item in desired) { - if (index < items.Count) + int itemIndex = items.IndexOf(item); + + if (itemIndex == -1) { - if (items[index] != i) - { - items[index] = i; - } + items.Insert(index, item); } - else + else if(itemIndex != index) { - items.Add(i); + items.RemoveAt(itemIndex); + items.Insert(index, item); } ++index; diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index ba4c5027d0..32e220b789 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -357,7 +357,7 @@ namespace Avalonia.Controls.Primitives if (control.TemplatedParent == this) { - foreach (IControl child in control.GetVisualChildren()) + foreach (IControl child in control.GetLogicalChildren()) { RegisterNames(child, nameScope); } diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index 6293cbfbfd..1ef03b49ce 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -39,21 +39,21 @@ namespace Avalonia.Controls.Remote.Server KeyboardDevice = AvaloniaLocator.Current.GetService(); } - private static RawMouseEventType GetAvaloniaEventType (Avalonia.Remote.Protocol.Input.MouseButton button, bool pressed) + private static RawPointerEventType GetAvaloniaEventType (Avalonia.Remote.Protocol.Input.MouseButton button, bool pressed) { switch (button) { case Avalonia.Remote.Protocol.Input.MouseButton.Left: - return pressed ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp; + return pressed ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp; case Avalonia.Remote.Protocol.Input.MouseButton.Middle: - return pressed ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp; + return pressed ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp; case Avalonia.Remote.Protocol.Input.MouseButton.Right: - return pressed ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp; + return pressed ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp; default: - return RawMouseEventType.Move; + return RawPointerEventType.Move; } } @@ -166,11 +166,11 @@ namespace Avalonia.Controls.Remote.Server { Dispatcher.UIThread.Post(() => { - Input?.Invoke(new RawMouseEventArgs( + Input?.Invoke(new RawPointerEventArgs( MouseDevice, 0, InputRoot, - RawMouseEventType.Move, + RawPointerEventType.Move, new Point(pointer.X, pointer.Y), GetAvaloniaInputModifiers(pointer.Modifiers))); }, DispatcherPriority.Input); @@ -179,7 +179,7 @@ namespace Avalonia.Controls.Remote.Server { Dispatcher.UIThread.Post(() => { - Input?.Invoke(new RawMouseEventArgs( + Input?.Invoke(new RawPointerEventArgs( MouseDevice, 0, InputRoot, @@ -192,7 +192,7 @@ namespace Avalonia.Controls.Remote.Server { Dispatcher.UIThread.Post(() => { - Input?.Invoke(new RawMouseEventArgs( + Input?.Invoke(new RawPointerEventArgs( MouseDevice, 0, InputRoot, diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs index 7307843417..ad7312d515 100644 --- a/src/Avalonia.Controls/RowDefinition.cs +++ b/src/Avalonia.Controls/RowDefinition.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public RowDefinition() + public RowDefinition() { } @@ -38,7 +38,7 @@ namespace Avalonia.Controls /// /// The height of the row. /// The height unit of the column. - public RowDefinition(double value, GridUnitType type) + public RowDefinition(double value, GridUnitType type) { Height = new GridLength(value, type); } @@ -47,7 +47,7 @@ namespace Avalonia.Controls /// Initializes a new instance of the class. /// /// The height of the column. - public RowDefinition(GridLength height) + public RowDefinition(GridLength height) { Height = height; } @@ -55,11 +55,7 @@ namespace Avalonia.Controls /// /// Gets the actual calculated height of the row. /// - public double ActualHeight - { - get; - internal set; - } + public double ActualHeight => Parent?.GetFinalRowDefinitionHeight(Index) ?? 0d; /// /// Gets or sets the maximum height of the row in DIPs. @@ -87,5 +83,9 @@ namespace Avalonia.Controls get { return GetValue(HeightProperty); } set { SetValue(HeightProperty, value); } } + + internal override GridLength UserSizeValueCache => this.Height; + internal override double UserMinSizeValueCache => this.MinHeight; + internal override double UserMaxSizeValueCache => this.MaxHeight; } } \ No newline at end of file diff --git a/src/Avalonia.Controls/RowDefinitions.cs b/src/Avalonia.Controls/RowDefinitions.cs index 1a14cc78f3..cf72cc8ba3 100644 --- a/src/Avalonia.Controls/RowDefinitions.cs +++ b/src/Avalonia.Controls/RowDefinitions.cs @@ -9,14 +9,13 @@ namespace Avalonia.Controls /// /// A collection of s. /// - public class RowDefinitions : AvaloniaList + public class RowDefinitions : DefinitionList { /// /// Initializes a new instance of the class. /// - public RowDefinitions() + public RowDefinitions() : base() { - ResetBehavior = ResetBehavior.Remove; } /// diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index d6537ebbca..fc2c118132 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.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 System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; @@ -8,6 +9,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -166,10 +168,24 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left) + if (e.MouseButton == MouseButton.Left && e.Pointer.Type == PointerType.Mouse) { e.Handled = UpdateSelectionFromEventSource(e.Source); } } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + if (e.MouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) + { + var container = GetContainerFromEventSource(e.Source); + if (container != null + && container.GetVisualsAt(e.GetPosition(container)) + .Any(c => container == c || container.IsVisualAncestorOf(c))) + { + e.Handled = UpdateSelectionFromEventSource(e.Source); + } + } + } } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 04b088e35c..7b4bb469d0 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -38,6 +38,15 @@ namespace Avalonia.Controls public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); + public static readonly StyledProperty SelectionBrushProperty = + AvaloniaProperty.Register(nameof(SelectionBrushProperty)); + + public static readonly StyledProperty SelectionForegroundBrushProperty = + AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); + + public static readonly StyledProperty CaretBrushProperty = + AvaloniaProperty.Register(nameof(CaretBrushProperty)); + public static readonly DirectProperty SelectionStartProperty = AvaloniaProperty.RegisterDirect( nameof(SelectionStart), @@ -169,6 +178,24 @@ namespace Avalonia.Controls set => SetValue(PasswordCharProperty, value); } + public IBrush SelectionBrush + { + get => GetValue(SelectionBrushProperty); + set => SetValue(SelectionBrushProperty, value); + } + + public IBrush SelectionForegroundBrush + { + get => GetValue(SelectionForegroundBrushProperty); + set => SetValue(SelectionForegroundBrushProperty, value); + } + + public IBrush CaretBrush + { + get => GetValue(CaretBrushProperty); + set => SetValue(CaretBrushProperty, value); + } + public int SelectionStart { get @@ -214,9 +241,9 @@ namespace Avalonia.Controls if (!_ignoreTextChanges) { var caretIndex = CaretIndex; - SelectionStart = CoerceCaretIndex(SelectionStart, value?.Length ?? 0); - SelectionEnd = CoerceCaretIndex(SelectionEnd, value?.Length ?? 0); - CaretIndex = CoerceCaretIndex(caretIndex, value?.Length ?? 0); + SelectionStart = CoerceCaretIndex(SelectionStart, value); + SelectionEnd = CoerceCaretIndex(SelectionEnd, value); + CaretIndex = CoerceCaretIndex(caretIndex, value); if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing) { @@ -287,16 +314,11 @@ namespace Avalonia.Controls { DecideCaretVisibility(); } - - e.Handled = true; } private void DecideCaretVisibility() { - if (!IsReadOnly) - _presenter?.ShowCaret(); - else - _presenter?.HideCaret(); + _presenter.ShowCaret(); } protected override void OnLostFocus(RoutedEventArgs e) @@ -456,7 +478,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; - + } else if (Match(keymap.MoveCursorToTheEndOfLine)) { @@ -485,7 +507,7 @@ namespace Avalonia.Controls movement = true; selection = true; handled = true; - + } else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection)) { @@ -677,11 +699,15 @@ namespace Avalonia.Controls } } - private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text?.Length ?? 0); + private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text); - private int CoerceCaretIndex(int value, int length) + private int CoerceCaretIndex(int value, string text) { - var text = Text; + if (text == null) + { + return 0; + } + var length = text.Length; if (value < 0) { @@ -691,7 +717,7 @@ namespace Avalonia.Controls { return length; } - else if (value > 0 && text[value - 1] == '\r' && text[value] == '\n') + else if (value > 0 && text[value - 1] == '\r' && value < length && text[value] == '\n') { return value + 1; } diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs deleted file mode 100644 index 7704228a4e..0000000000 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ /dev/null @@ -1,705 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; -using Avalonia.Layout; -using JetBrains.Annotations; - -namespace Avalonia.Controls.Utils -{ - /// - /// Contains algorithms that can help to measure and arrange a Grid. - /// - internal class GridLayout - { - /// - /// Initialize a new instance from the column definitions. - /// The instance doesn't care about whether the definitions are rows or columns. - /// It will not calculate the column or row differently. - /// - internal GridLayout([NotNull] ColumnDefinitions columns) - { - if (columns == null) throw new ArgumentNullException(nameof(columns)); - _conventions = columns.Count == 0 - ? new List { new LengthConvention() } - : columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList(); - } - - /// - /// Initialize a new instance from the row definitions. - /// The instance doesn't care about whether the definitions are rows or columns. - /// It will not calculate the column or row differently. - /// - internal GridLayout([NotNull] RowDefinitions rows) - { - if (rows == null) throw new ArgumentNullException(nameof(rows)); - _conventions = rows.Count == 0 - ? new List { new LengthConvention() } - : rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList(); - } - - /// - /// Gets the layout tolerance. If any length offset is less than this value, we will treat them the same. - /// - private const double LayoutTolerance = 1.0 / 256.0; - - /// - /// Gets all the length conventions that come from column/row definitions. - /// These conventions provide cell limitations, such as the expected pixel length, the min/max pixel length and the * count. - /// - [NotNull] - private readonly List _conventions; - - /// - /// Gets all the length conventions that come from the grid children. - /// - [NotNull] - private readonly List _additionalConventions = - new List(); - - /// - /// Appending these elements into the convention list helps lay them out according to their desired sizes. - /// - /// Some elements are not only in a single grid cell, they have one or more column/row spans, - /// and these elements may affect the grid layout especially the measuring procedure. - /// Append these elements into the convention list can help to layout them correctly through - /// their desired size. Only a small subset of children need to be measured before layout starts - /// and they will be called via the callback. - /// - /// The grid children type. - /// - /// Contains the safe column/row index and its span. - /// Notice that we will not verify whether the range is in the column/row count, - /// so you should get the safe column/row info first. - /// - /// - /// This callback will be called if the thinks that a child should be - /// measured first. Usually, these are the children that have the * or Auto length. - /// - internal void AppendMeasureConventions([NotNull] IDictionary source, - [NotNull] Func getDesiredLength) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (getDesiredLength == null) throw new ArgumentNullException(nameof(getDesiredLength)); - - // M1/7. Find all the Auto and * length columns/rows. (M1/7 means the 1st procedure of measurement.) - // Only these columns/rows' layout can be affected by the child desired size. - // - // Find all columns/rows that have Auto or * length. We'll measure the children in advance. - // Only these kind of columns/rows will affect the Grid layout. - // Please note: - // - If the column / row has Auto length, the Grid.DesiredSize and the column width - // will be affected by the child's desired size. - // - If the column / row has* length, the Grid.DesiredSize will be affected by the - // child's desired size but the column width not. - - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // _conventions: | min | max | | | min | | min max | max | - // _additionalC: |<- desired ->| |< desired >| - // _additionalC: |< desired >| |<- desired ->| - - // 寻找所有行列范围中包含 Auto 和 * 的元素,使用全部可用尺寸提前测量。 - // 因为只有这部分元素的布局才会被 Grid 的子元素尺寸影响。 - // 请注意: - // - Auto 长度的行列必定会受到子元素布局影响,会影响到行列的布局长度和 Grid 本身的 DesiredSize; - // - 而对于 * 长度,只有 Grid.DesiredSize 会受到子元素布局影响,而行列长度不会受影响。 - - // Find all the Auto and * length columns/rows. - var found = new Dictionary(); - for (var i = 0; i < _conventions.Count; i++) - { - var index = i; - var convention = _conventions[index]; - if (convention.Length.IsAuto || convention.Length.IsStar) - { - foreach (var pair in source.Where(x => - x.Value.index <= index && index < x.Value.index + x.Value.span)) - { - found[pair.Key] = pair.Value; - } - } - } - - // Append these layout into the additional convention list. - foreach (var pair in found) - { - var t = pair.Key; - var (index, span) = pair.Value; - var desiredLength = getDesiredLength(t); - if (Math.Abs(desiredLength) > LayoutTolerance) - { - _additionalConventions.Add(new AdditionalLengthConvention(index, span, desiredLength)); - } - } - } - - /// - /// Run measure procedure according to the and gets the . - /// - /// - /// The container length. Usually, it is the constraint of the method. - /// - /// - /// Overriding conventions that allows the algorithm to handle external inputa - /// - /// - /// The measured result that containing the desired size and all the column/row lengths. - /// - [NotNull, Pure] - internal MeasureResult Measure(double containerLength, IReadOnlyList conventions = null) - { - // Prepare all the variables that this method needs to use. - conventions = conventions ?? _conventions.Select(x => x.Clone()).ToList(); - var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); - var aggregatedLength = 0.0; - double starUnitLength; - - // M2/7. Aggregate all the pixel lengths. Then we can get the remaining length by `containerLength - aggregatedLength`. - // We mark the aggregated length as "fix" because we can completely determine their values. Same as below. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // |#fix#| |#fix#| - // - // 将全部的固定像素长度的行列长度累加。这样,containerLength - aggregatedLength 便能得到剩余长度。 - // 我们会将所有能够确定下长度的行列标记为 fix。下同。 - // 请注意: - // - 我们并没有直接从 containerLength 一直减下去,而是使用 aggregatedLength 进行累加,是因为无穷大相减得到的是 NaN,不利于后续计算。 - - aggregatedLength += conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - - // M3/7. Fix all the * lengths that have reached the minimum. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // | fix | |#fix#| fix | - - var shouldTestStarMin = true; - while (shouldTestStarMin) - { - // Calculate the unit * length to estimate the length of each column/row that has * length. - // Under this estimated length, check if there is a minimum value that has a length less than its constraint. - // If there is such a *, then fix the size of this cell, and then loop it again until there is no * that can be constrained by the minimum value. - // - // 计算单位 * 的长度,以便预估出每一个 * 行列的长度。 - // 在此预估的长度下,从前往后寻找是否存在某个 * 长度已经小于其约束的最小值。 - // 如果发现存在这样的 *,那么将此单元格的尺寸固定下来(Fix),然后循环重来,直至再也没有能被最小值约束的 *。 - var @fixed = false; - starUnitLength = (containerLength - aggregatedLength) / starCount; - foreach (var convention in conventions.Where(x => x.Length.IsStar)) - { - var (star, min) = (convention.Length.Value, convention.MinLength); - var starLength = star * starUnitLength; - if (starLength < min) - { - convention.Fix(min); - starLength = min; - aggregatedLength += starLength; - starCount -= star; - @fixed = true; - break; - } - } - - shouldTestStarMin = @fixed; - } - - // M4/7. Determine the absolute pixel size of all columns/rows that have an Auto length. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#fix#| | fix |#fix#| fix | fix | - - var shouldTestAuto = true; - while (shouldTestAuto) - { - var @fixed = false; - starUnitLength = (containerLength - aggregatedLength) / starCount; - for (var i = 0; i < conventions.Count; i++) - { - var convention = conventions[i]; - if (!convention.Length.IsAuto) - { - continue; - } - - var more = ApplyAdditionalConventionsForAuto(conventions, i, starUnitLength); - convention.Fix(more); - aggregatedLength += more; - @fixed = true; - break; - } - - shouldTestAuto = @fixed; - } - - // M5/7. Expand the stars according to the additional conventions (usually the child desired length). - // We can't fix this kind of length, so we just mark them as desired (des). - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| - - var (minLengths, desiredStarMin) = AggregateAdditionalConventionsForStars(conventions); - aggregatedLength += desiredStarMin; - - // M6/7. Determine the desired length of the grid for current container length. Its value is stored in desiredLength. - // Assume if the container has infinite length, the grid desired length is stored in greedyDesiredLength. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| - // Note: This table will be stored as the intermediate result into the MeasureResult and it will be reused by Arrange procedure. - // - // desiredLength = Math.Max(0.0, des + fix + des + fix + fix + fix + fix + des + des) - // greedyDesiredLength = des + fix + des + fix + fix + fix + fix + des + des - - var desiredLength = containerLength - aggregatedLength >= 0.0 ? aggregatedLength : containerLength; - var greedyDesiredLength = aggregatedLength; - - // M7/7. Expand all the rest stars. These stars have no conventions or only have - // max value they can be expanded from zero to constraint. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#fix#| fix |#fix#| fix | fix | fix | fix | #fix# |#fix#| - // Note: This table will be stored as the final result into the MeasureResult. - - var dynamicConvention = ExpandStars(conventions, containerLength); - Clip(dynamicConvention, containerLength); - - // Returns the measuring result. - return new MeasureResult(containerLength, desiredLength, greedyDesiredLength, - conventions, dynamicConvention, minLengths); - } - - /// - /// Run arrange procedure according to the and gets the . - /// - /// - /// The container length. Usually, it is the finalSize of the method. - /// - /// - /// The result that the measuring procedure returns. If it is null, a new measure procedure will run. - /// - /// - /// The measured result that containing the desired size and all the column/row length. - /// - [NotNull, Pure] - public ArrangeResult Arrange(double finalLength, [CanBeNull] MeasureResult measure) - { - measure = measure ?? Measure(finalLength); - - // If the arrange final length does not equal to the measure length, we should measure again. - if (finalLength - measure.ContainerLength > LayoutTolerance) - { - // If the final length is larger, we will rerun the whole measure. - measure = Measure(finalLength, measure.LeanLengthList); - } - else if (finalLength - measure.ContainerLength < -LayoutTolerance) - { - // If the final length is smaller, we measure the M6/6 procedure only. - var dynamicConvention = ExpandStars(measure.LeanLengthList, finalLength); - measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength, - measure.LeanLengthList, dynamicConvention, measure.MinLengths); - } - - return new ArrangeResult(measure.LengthList); - } - - /// - /// Use the to calculate the fixed length of the Auto column/row. - /// - /// The convention list that all the * with minimum length are fixed. - /// The column/row index that should be fixed. - /// The unit * length for the current rest length. - /// The final length of the Auto length column/row. - [Pure] - private double ApplyAdditionalConventionsForAuto(IReadOnlyList conventions, - int index, double starUnitLength) - { - // 1. Calculate all the * length with starUnitLength. - // 2. Exclude all the fixed length and all the * length. - // 3. Compare the rest of the desired length and the convention. - // +-----------------+ - // | * | A | * | - // +-----------------+ - // | exl | | exl | - // |< desired >| - // |< desired >| - - var more = 0.0; - foreach (var additional in _additionalConventions) - { - // If the additional convention's last column/row contains the Auto column/row, try to determine the Auto column/row length. - if (index == additional.Index + additional.Span - 1) - { - var min = Enumerable.Range(additional.Index, additional.Span) - .Select(x => - { - var c = conventions[x]; - if (c.Length.IsAbsolute) return c.Length.Value; - if (c.Length.IsStar) return c.Length.Value * starUnitLength; - return 0.0; - }).Sum(); - more = Math.Max(additional.Min - min, more); - } - } - - return Math.Min(conventions[index].MaxLength, more); - } - - /// - /// Calculate the total desired length of all the * length. - /// Bug Warning: - /// - The behavior of this method is undefined! Different UI Frameworks have different behaviors. - /// - We ignore all the span columns/rows and just take single cells into consideration. - /// - /// All the conventions that have almost been fixed except the rest *. - /// The total desired length of all the * length. - [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] - private (List, double) AggregateAdditionalConventionsForStars( - IReadOnlyList conventions) - { - // 1. Determine all one-span column's desired widths or row's desired heights. - // 2. Order the multi-span conventions by its last index - // (Notice that the sorted data is much smaller than the source.) - // 3. Determine each multi-span last index by calculating the maximum desired size. - - // Before we determine the behavior of this method, we just aggregate the one-span * columns. - - var fixedLength = conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - - // Prepare a lengthList variable indicating the fixed length of each column/row. - var lengthList = conventions.Select(x => x.Length.IsAbsolute ? x.Length.Value : 0.0).ToList(); - foreach (var group in _additionalConventions - .Where(x => x.Span == 1 && conventions[x.Index].Length.IsStar) - .ToLookup(x => x.Index)) - { - lengthList[group.Key] = Math.Max(lengthList[group.Key], group.Max(x => x.Min)); - } - - // Now the lengthList is fixed by every one-span columns/rows. - // Then we should determine the multi-span column's/row's length. - foreach (var group in _additionalConventions - .Where(x => x.Span > 1) - .ToLookup(x => x.Index + x.Span - 1) - // Order the multi-span columns/rows by last index. - .OrderBy(x => x.Key)) - { - var length = group.Max(x => x.Min - Enumerable.Range(x.Index, x.Span - 1).Sum(r => lengthList[r])); - lengthList[group.Key] = Math.Max(lengthList[group.Key], length > 0 ? length : 0); - } - - return (lengthList, lengthList.Sum() - fixedLength); - } - - /// - /// This method implements the last procedure (M7/7) of measure. - /// It expands all the * length to the fixed length according to the . - /// - /// All the conventions that have almost been fixed except the remaining *. - /// The container length. - /// The final pixel length list. - [Pure] - private static List ExpandStars(IEnumerable conventions, double constraint) - { - // Initial. - var dynamicConvention = conventions.Select(x => x.Clone()).ToList(); - constraint -= dynamicConvention.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - var starUnitLength = 0.0; - - // M6/6. - if (constraint >= 0) - { - var starCount = dynamicConvention.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); - - var shouldTestStarMax = true; - while (shouldTestStarMax) - { - var @fixed = false; - starUnitLength = constraint / starCount; - foreach (var convention in dynamicConvention.Where(x => - x.Length.IsStar && !double.IsPositiveInfinity(x.MaxLength))) - { - var (star, max) = (convention.Length.Value, convention.MaxLength); - var starLength = star * starUnitLength; - if (starLength > max) - { - convention.Fix(max); - starLength = max; - constraint -= starLength; - starCount -= star; - @fixed = true; - break; - } - } - - shouldTestStarMax = @fixed; - } - } - - Debug.Assert(dynamicConvention.All(x => !x.Length.IsAuto)); - - var starUnit = starUnitLength; - var result = dynamicConvention.Select(x => - { - if (x.Length.IsStar) - { - return double.IsInfinity(starUnit) ? double.PositiveInfinity : starUnit * x.Length.Value; - } - - return x.Length.Value; - }).ToList(); - - return result; - } - - /// - /// If the container length is not infinity. It may be not enough to contain all the columns/rows. - /// We should clip the columns/rows that have been out of the container bounds. - /// Note: This method may change the items value of . - /// - /// A list of all the column widths and row heights with a fixed pixel length - /// the container length. It can be positive infinity. - private static void Clip([NotNull] IList lengthList, double constraint) - { - if (double.IsInfinity(constraint)) - { - return; - } - - var measureLength = 0.0; - for (var i = 0; i < lengthList.Count; i++) - { - var length = lengthList[i]; - if (constraint - measureLength > length) - { - measureLength += length; - } - else - { - lengthList[i] = constraint - measureLength; - measureLength = constraint; - } - } - } - - /// - /// Contains the convention of each column/row. - /// This is mostly the same as or . - /// We use this because we can treat the column and the row the same. - /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - internal class LengthConvention : ICloneable - { - /// - /// Initialize a new instance of . - /// - public LengthConvention() - { - Length = new GridLength(1.0, GridUnitType.Star); - MinLength = 0.0; - MaxLength = double.PositiveInfinity; - } - - /// - /// Initialize a new instance of . - /// - public LengthConvention(GridLength length, double minLength, double maxLength) - { - Length = length; - MinLength = minLength; - MaxLength = maxLength; - if (length.IsAbsolute) - { - _isFixed = true; - } - } - - /// - /// Gets the of a column or a row. - /// - internal GridLength Length { get; private set; } - - /// - /// Gets the minimum convention for a column or a row. - /// - internal double MinLength { get; } - - /// - /// Gets the maximum convention for a column or a row. - /// - internal double MaxLength { get; } - - /// - /// Fix the . - /// If all columns/rows are fixed, we can get the size of all columns/rows in pixels. - /// - /// - /// The pixel length that should be used to fix the convention. - /// - /// - /// If the convention is pixel length, this exception will throw. - /// - public void Fix(double pixel) - { - if (_isFixed) - { - throw new InvalidOperationException("Cannot fix the length convention if it is fixed."); - } - - Length = new GridLength(pixel); - _isFixed = true; - } - - /// - /// Gets a value that indicates whether this convention is fixed. - /// - private bool _isFixed; - - /// - /// Helps the debugger to display the intermediate column/row calculation result. - /// - private string DebuggerDisplay => - $"{(_isFixed ? Length.Value.ToString(CultureInfo.InvariantCulture) : (Length.GridUnitType == GridUnitType.Auto ? "Auto" : $"{Length.Value}*"))}, ∈[{MinLength}, {MaxLength}]"; - - /// - object ICloneable.Clone() => Clone(); - - /// - /// Get a deep copy of this convention list. - /// We need this because we want to store some intermediate states. - /// - internal LengthConvention Clone() => new LengthConvention(Length, MinLength, MaxLength); - } - - /// - /// Contains the convention that comes from the grid children. - /// Some children span multiple columns or rows, so even a simple column/row can have multiple conventions. - /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - internal struct AdditionalLengthConvention - { - /// - /// Initialize a new instance of . - /// - public AdditionalLengthConvention(int index, int span, double min) - { - Index = index; - Span = span; - Min = min; - } - - /// - /// Gets the start index of this additional convention. - /// - public int Index { get; } - - /// - /// Gets the span of this additional convention. - /// - public int Span { get; } - - /// - /// Gets the minimum length of this additional convention. - /// This value is usually provided by the child's desired length. - /// - public double Min { get; } - - /// - /// Helps the debugger to display the intermediate column/row calculation result. - /// - private string DebuggerDisplay => - $"{{{string.Join(",", Enumerable.Range(Index, Span))}}}, ∈[{Min},∞)"; - } - - /// - /// Stores the result of the measuring procedure. - /// This result can be used to measure children and assign the desired size. - /// Passing this result to can reduce calculation. - /// - [DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] - internal class MeasureResult - { - /// - /// Initialize a new instance of . - /// - internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength, - IReadOnlyList leanConventions, IReadOnlyList expandedConventions, IReadOnlyList minLengths) - { - ContainerLength = containerLength; - DesiredLength = desiredLength; - GreedyDesiredLength = greedyDesiredLength; - LeanLengthList = leanConventions; - LengthList = expandedConventions; - MinLengths = minLengths; - } - - /// - /// Gets the container length for this result. - /// This property will be used by to determine whether to measure again or not. - /// - public double ContainerLength { get; } - - /// - /// Gets the desired length of this result. - /// Just return this value as the desired size in . - /// - public double DesiredLength { get; } - - /// - /// Gets the desired length if the container has infinite length. - /// - public double GreedyDesiredLength { get; } - - /// - /// Contains the column/row calculation intermediate result. - /// This value is used by for reducing repeat calculation. - /// - public IReadOnlyList LeanLengthList { get; } - - /// - /// Gets the length list for each column/row. - /// - public IReadOnlyList LengthList { get; } - public IReadOnlyList MinLengths { get; } - } - - /// - /// Stores the result of the measuring procedure. - /// This result can be used to arrange children and assign the render size. - /// - [DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] - internal class ArrangeResult - { - /// - /// Initialize a new instance of . - /// - internal ArrangeResult(IReadOnlyList lengthList) - { - LengthList = lengthList; - } - - /// - /// Gets the length list for each column/row. - /// - public IReadOnlyList LengthList { get; } - } - } -} diff --git a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs deleted file mode 100644 index 8553165e4b..0000000000 --- a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs +++ /dev/null @@ -1,651 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Subjects; -using Avalonia.Collections; -using Avalonia.Controls.Utils; -using Avalonia.Layout; -using Avalonia.VisualTree; - -namespace Avalonia.Controls -{ - /// - /// Shared size scope implementation. - /// Shares the size information between participating grids. - /// An instance of this class is attached to every that has its - /// IsSharedSizeScope property set to true. - /// - internal sealed class SharedSizeScopeHost : IDisposable - { - private enum MeasurementState - { - Invalidated, - Measuring, - Cached - } - - /// - /// Class containing the measured rows/columns for a single grid. - /// Monitors changes to the row/column collections as well as the SharedSizeGroup changes - /// for the individual items in those collections. - /// Notifies the of SharedSizeGroup changes. - /// - private sealed class MeasurementCache : IDisposable - { - readonly CompositeDisposable _subscriptions; - readonly Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>(); - - public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged; - - public MeasurementCache(Grid grid) - { - Grid = grid; - Results = grid.RowDefinitions.Cast() - .Concat(grid.ColumnDefinitions) - .Select(d => new MeasurementResult(grid, d)) - .ToList(); - - grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged; - grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged; - - - _subscriptions = new CompositeDisposable( - Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged), - Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged), - grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged), - grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged)); - - } - - // method to be hooked up once RowDefinitions/ColumnDefinitions collections can be replaced on a grid - private void DefinitionsChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - // route to collection changed as a Reset. - DefinitionsCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - private void DefinitionPropertyChanged(Tuple propertyChanged) - { - if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup)) - { - var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item1)); - var oldName = result.SizeGroup?.Name; - var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup; - _groupChanged.OnNext((oldName, newName, result)); - } - } - - private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - int offset = 0; - if (sender is ColumnDefinitions) - offset = Grid.RowDefinitions.Count; - - var newItems = e.NewItems?.OfType().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List(); - var oldItems = e.OldStartingIndex >= 0 - ? Results.GetRange(e.OldStartingIndex + offset, e.OldItems.Count) - : new List(); - - void NotifyNewItems() - { - foreach (var item in newItems) - { - if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) - continue; - - _groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item)); - } - } - - void NotifyOldItems() - { - foreach (var item in oldItems) - { - if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) - continue; - - _groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item)); - } - } - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - Results.InsertRange(e.NewStartingIndex + offset, newItems); - NotifyNewItems(); - break; - - case NotifyCollectionChangedAction.Remove: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - NotifyOldItems(); - break; - - case NotifyCollectionChangedAction.Move: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - Results.InsertRange(e.NewStartingIndex + offset, oldItems); - break; - - case NotifyCollectionChangedAction.Replace: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - Results.InsertRange(e.NewStartingIndex + offset, newItems); - - NotifyOldItems(); - NotifyNewItems(); - - break; - - case NotifyCollectionChangedAction.Reset: - oldItems = Results; - newItems = Results = Grid.RowDefinitions.Cast() - .Concat(Grid.ColumnDefinitions) - .Select(d => new MeasurementResult(Grid, d)) - .ToList(); - NotifyOldItems(); - NotifyNewItems(); - - break; - } - } - - - /// - /// Updates the Results collection with Grid Measure results. - /// - /// Result of the GridLayout.Measure method for the RowDefinitions in the grid. - /// Result of the GridLayout.Measure method for the ColumnDefinitions in the grid. - public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - MeasurementState = MeasurementState.Cached; - for (int i = 0; i < Grid.RowDefinitions.Count; i++) - { - Results[i].MeasuredResult = rowResult.LengthList[i]; - Results[i].MinLength = rowResult.MinLengths[i]; - } - - for (int i = 0; i < Grid.ColumnDefinitions.Count; i++) - { - Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i]; - Results[i + Grid.RowDefinitions.Count].MinLength = columnResult.MinLengths[i]; - } - } - - /// - /// Clears the measurement cache, in preparation for the Measure pass. - /// - public void InvalidateMeasure() - { - var newItems = new List(); - var oldItems = new List(); - - MeasurementState = MeasurementState.Invalidated; - - Results.ForEach(r => - { - r.MeasuredResult = double.NaN; - r.SizeGroup?.Reset(); - }); - } - - /// - /// Clears the subscriptions. - /// - public void Dispose() - { - _subscriptions.Dispose(); - _groupChanged.OnCompleted(); - } - - /// - /// Gets the for which this cache has been created. - /// - public Grid Grid { get; } - - /// - /// Gets the of this cache. - /// - public MeasurementState MeasurementState { get; private set; } - - /// - /// Gets the list of instances. - /// - /// - /// The list is a 1-1 map of the concatenation of RowDefinitions and ColumnDefinitions - /// - public List Results { get; private set; } - } - - - /// - /// Class containing the Measure result for a single Row/Column in a grid. - /// - private class MeasurementResult - { - public MeasurementResult(Grid owningGrid, DefinitionBase definition) - { - OwningGrid = owningGrid; - Definition = definition; - MeasuredResult = double.NaN; - } - - /// - /// Gets the / related to this - /// - public DefinitionBase Definition { get; } - - /// - /// Gets or sets the actual result of the Measure operation for this column. - /// - public double MeasuredResult { get; set; } - - /// - /// Gets or sets the Minimum constraint for a Row/Column - relevant for star Rows/Columns in unconstrained grids. - /// - public double MinLength { get; set; } - - /// - /// Gets or sets the that this result belongs to. - /// - public Group SizeGroup { get; set; } - - /// - /// Gets the Grid that is the parent of the Row/Column - /// - public Grid OwningGrid { get; } - - /// - /// Calculates the effective length that this Row/Column wishes to enforce in the SharedSizeGroup. - /// - /// A tuple of length and the priority in the shared size group. - public (double length, int priority) GetPriorityLength() - { - var length = (Definition as ColumnDefinition)?.Width ?? ((RowDefinition)Definition).Height; - - if (length.IsAbsolute) - return (MeasuredResult, 1); - if (length.IsAuto) - return (MeasuredResult, 2); - if (MinLength > 0) - return (MinLength, 3); - return (MeasuredResult, 4); - } - } - - /// - /// Visitor class used to gather the final length for a given SharedSizeGroup. - /// - /// - /// The values are applied according to priorities defined in . - /// - private class LentgthGatherer - { - /// - /// Gets the final Length to be applied to every Row/Column in a SharedSizeGroup - /// - public double Length { get; private set; } - private int gatheredPriority = 6; - - /// - /// Visits the applying the result of to its internal cache. - /// - /// The instance to visit. - public void Visit(MeasurementResult result) - { - var (length, priority) = result.GetPriorityLength(); - - if (gatheredPriority < priority) - return; - - gatheredPriority = priority; - if (gatheredPriority == priority) - { - Length = Math.Max(length,Length); - } - else - { - Length = length; - } - } - } - - /// - /// Representation of a SharedSizeGroup, containing Rows/Columns with the same SharedSizeGroup property value. - /// - private class Group - { - private double? cachedResult; - private List _results = new List(); - - /// - /// Gets the name of the SharedSizeGroup. - /// - public string Name { get; } - - public Group(string name) - { - Name = name; - } - - /// - /// Gets the collection of the instances. - /// - public IReadOnlyList Results => _results; - - /// - /// Gets the final, calculated length for all Rows/Columns in the SharedSizeGroup. - /// - public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value; - - /// - /// Clears the previously cached result in preparation for measurement. - /// - public void Reset() - { - cachedResult = null; - } - - /// - /// Ads a measurement result to this group and sets it's property - /// to this instance. - /// - /// The to include in this group. - public void Add(MeasurementResult result) - { - if (_results.Contains(result)) - throw new AvaloniaInternalException( - $"SharedSizeScopeHost: Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result"); - - result.SizeGroup = this; - _results.Add(result); - } - - /// - /// Removes the measurement result from this group and clears its value. - /// - /// The to clear. - public void Remove(MeasurementResult result) - { - if (!_results.Contains(result)) - throw new AvaloniaInternalException( - $"SharedSizeScopeHost: Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result"); - result.SizeGroup = null; - _results.Remove(result); - } - - - private double Gather() - { - var visitor = new LentgthGatherer(); - - _results.ForEach(visitor.Visit); - - return visitor.Length; - } - } - - private readonly AvaloniaList _measurementCaches = new AvaloniaList(); - private readonly Dictionary _groups = new Dictionary(); - private bool _invalidating; - - /// - /// Removes the SharedSizeScope and notifies all affected grids of the change. - /// - public void Dispose() - { - while (_measurementCaches.Any()) - _measurementCaches[0].Grid.SharedScopeChanged(); - } - - /// - /// Registers the grid in this SharedSizeScope, to be called when the grid is added to the visual tree. - /// - /// The to add to this scope. - internal void RegisterGrid(Grid toAdd) - { - if (_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd))) - throw new AvaloniaInternalException("SharedSizeScopeHost: tried to register a grid twice!"); - - var cache = new MeasurementCache(toAdd); - _measurementCaches.Add(cache); - AddGridToScopes(cache); - } - - /// - /// Removes the registration for a grid in this SharedSizeScope. - /// - /// The to remove. - internal void UnegisterGrid(Grid toRemove) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove)); - if (cache == null) - throw new AvaloniaInternalException("SharedSizeScopeHost: tried to unregister a grid that wasn't registered before!"); - - _measurementCaches.Remove(cache); - RemoveGridFromScopes(cache); - cache.Dispose(); - } - - /// - /// Helper method to check if a grid needs to forward its Mesure results to, and requrest Arrange results from this scope. - /// - /// The that should be checked. - /// True if the grid should forward its calls. - internal bool ParticipatesInScope(Grid toCheck) - { - return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck)) - ?.Results.Any(r => r.SizeGroup != null) ?? false; - } - - /// - /// Notifies the SharedSizeScope that a grid had requested its measurement to be invalidated. - /// Forwards the same call to all affected grids in this scope. - /// - /// The that had it's Measure invalidated. - internal void InvalidateMeasure(Grid grid) - { - // prevent stack overflow - if (_invalidating) - return; - _invalidating = true; - - InvalidateMeasureImpl(grid); - - _invalidating = false; - } - - /// - /// Updates the measurement cache with the results of the measurement pass. - /// - /// The that has been measured. - /// Measurement result for the grid's - /// Measurement result for the grid's - internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - if (cache == null) - throw new AvaloniaInternalException("SharedSizeScopeHost: Attempted to update measurement status for a grid that wasn't registered!"); - - cache.UpdateMeasureResult(rowResult, columnResult); - } - - /// - /// Calculates the measurement result including the impact of any SharedSizeGroups that might affect this grid. - /// - /// The that is being Arranged - /// The 's cached measurement result. - /// The 's cached measurement result. - /// Row and column measurement result updated with the SharedSizeScope constraints. - internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - return ( - Arrange(grid.RowDefinitions, rowResult), - Arrange(grid.ColumnDefinitions, columnResult) - ); - } - - /// - /// Invalidates the measure of all grids affected by the SharedSizeGroups contained within. - /// - /// The that is being invalidated. - private void InvalidateMeasureImpl(Grid grid) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - - if (cache == null) - throw new AvaloniaInternalException( - $"SharedSizeScopeHost: InvalidateMeasureImpl - called with a grid not present in the internal cache"); - - // already invalidated the cache, early out. - if (cache.MeasurementState == MeasurementState.Invalidated) - return; - - // we won't calculate, so we should not invalidate. - if (!ParticipatesInScope(grid)) - return; - - cache.InvalidateMeasure(); - - // maybe there is a condition to only call arrange on some of the calls? - grid.InvalidateMeasure(); - - // find all the scopes within the invalidated grid - var scopeNames = cache.Results - .Where(mr => mr.SizeGroup != null) - .Select(mr => mr.SizeGroup.Name) - .Distinct(); - // find all grids related to those scopes - var otherGrids = scopeNames.SelectMany(sn => _groups[sn].Results) - .Select(r => r.OwningGrid) - .Where(g => g.IsMeasureValid) - .Distinct(); - - // invalidate them as well - foreach (var otherGrid in otherGrids) - { - InvalidateMeasureImpl(otherGrid); - } - } - - /// - /// callback notifying the scope that a has changed its - /// SharedSizeGroup - /// - /// Old and New name (either can be null) of the SharedSizeGroup, as well as the result. - private void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) - { - RemoveFromGroup(change.oldName, change.result); - AddToGroup(change.newName, change.result); - } - - /// - /// Handles the impact of SharedSizeGroups on the Arrange of / - /// - /// Rows/Columns that were measured - /// The initial measurement result. - /// Modified measure result - private GridLayout.MeasureResult Arrange(IReadOnlyList definitions, GridLayout.MeasureResult measureResult) - { - var conventions = measureResult.LeanLengthList.ToList(); - var lengths = measureResult.LengthList.ToList(); - var desiredLength = 0.0; - for (int i = 0; i < definitions.Count; i++) - { - var definition = definitions[i]; - - // for empty SharedSizeGroups pass on unmodified result. - if (string.IsNullOrEmpty(definition.SharedSizeGroup)) - { - desiredLength += measureResult.LengthList[i]; - continue; - } - - var group = _groups[definition.SharedSizeGroup]; - // Length calculated over all Definitions participating in a SharedSizeGroup. - var length = group.CalculatedLength; - - conventions[i] = new GridLayout.LengthConvention( - new GridLength(length), - measureResult.LeanLengthList[i].MinLength, - measureResult.LeanLengthList[i].MaxLength - ); - lengths[i] = length; - desiredLength += length; - } - - return new GridLayout.MeasureResult( - measureResult.ContainerLength, - desiredLength, - measureResult.GreedyDesiredLength,//?? - conventions, - lengths, - measureResult.MinLengths); - } - - /// - /// Adds all measurement results for a grid to their repsective scopes. - /// - /// The for a grid to be added. - private void AddGridToScopes(MeasurementCache cache) - { - cache.GroupChanged.Subscribe(SharedGroupChanged); - - foreach (var result in cache.Results) - { - var scopeName = result.Definition.SharedSizeGroup; - AddToGroup(scopeName, result); - } - } - - /// - /// Handles adding the to a SharedSizeGroup. - /// Does nothing for empty SharedSizeGroups. - /// - /// The name (can be null or empty) of the group to add the to. - /// The to add to a scope. - private void AddToGroup(string scopeName, MeasurementResult result) - { - if (string.IsNullOrEmpty(scopeName)) - return; - - if (!_groups.TryGetValue(scopeName, out var group)) - _groups.Add(scopeName, group = new Group(scopeName)); - - group.Add(result); - } - - /// - /// Removes all measurement results for a grid from their respective scopes. - /// - /// The for a grid to be removed. - private void RemoveGridFromScopes(MeasurementCache cache) - { - foreach (var result in cache.Results) - { - var scopeName = result.Definition.SharedSizeGroup; - RemoveFromGroup(scopeName, result); - } - } - - /// - /// Handles removing the from a SharedSizeGroup. - /// Does nothing for empty SharedSizeGroups. - /// - /// The name (can be null or empty) of the group to remove the from. - /// The to remove from a scope. - private void RemoveFromGroup(string scopeName, MeasurementResult result) - { - if (string.IsNullOrEmpty(scopeName)) - return; - - if (!_groups.TryGetValue(scopeName, out var group)) - throw new AvaloniaInternalException($"SharedSizeScopeHost: The scope {scopeName} wasn't found in the shared size scope"); - - group.Remove(result); - if (!group.Results.Any()) - _groups.Remove(scopeName); - } - } -} diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 01c9a3a110..01614ba87b 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -330,8 +330,7 @@ namespace Avalonia.Controls protected virtual bool HandleClosing() { var args = new CancelEventArgs(); - Closing?.Invoke(this, args); - + OnClosing(args); return args.Cancel; } @@ -576,6 +575,17 @@ namespace Avalonia.Controls base.HandleResized(clientSize); } + + /// + /// Raises the event. + /// + /// The event args. + /// + /// A type that derives from may override . The + /// overridden method must call on the base class if the + /// event needs to be raised. + /// + protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); } } diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 597734d400..4df1b39400 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using Avalonia.Input; +using Avalonia.Utilities; using static System.Math; @@ -92,109 +93,127 @@ namespace Avalonia.Controls } } - private UVSize CreateUVSize(Size size) => new UVSize(Orientation, size); - - private UVSize CreateUVSize() => new UVSize(Orientation); - /// - protected override Size MeasureOverride(Size availableSize) + protected override Size MeasureOverride(Size constraint) { - var desiredSize = CreateUVSize(); - var lineSize = CreateUVSize(); - var uvAvailableSize = CreateUVSize(availableSize); + var curLineSize = new UVSize(Orientation); + var panelSize = new UVSize(Orientation); + var uvConstraint = new UVSize(Orientation, constraint.Width, constraint.Height); - foreach (var child in Children) + var childConstraint = new Size(constraint.Width, constraint.Height); + + for (int i = 0, count = Children.Count; i < count; i++) { - child.Measure(availableSize); - var childSize = CreateUVSize(child.DesiredSize); - if (lineSize.U + childSize.U <= uvAvailableSize.U) // same line + var child = Children[i]; + if (child == null) continue; + + //Flow passes its own constrint to children + child.Measure(childConstraint); + + //this is the size of the child in UV space + var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height); + + if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U)) //need to switch to another line { - lineSize.U += childSize.U; - lineSize.V = Max(lineSize.V, childSize.V); + panelSize.U = Max(curLineSize.U, panelSize.U); + panelSize.V += curLineSize.V; + curLineSize = sz; + + if (MathUtilities.GreaterThan(sz.U, uvConstraint.U)) //the element is wider then the constrint - give it a separate line + { + panelSize.U = Max(sz.U, panelSize.U); + panelSize.V += sz.V; + curLineSize = new UVSize(Orientation); + } } - else // moving to next line + else //continue to accumulate a line { - desiredSize.U = Max(lineSize.U, uvAvailableSize.U); - desiredSize.V += lineSize.V; - lineSize = childSize; + curLineSize.U += sz.U; + curLineSize.V = Max(sz.V, curLineSize.V); } } - // last element - desiredSize.U = Max(lineSize.U, desiredSize.U); - desiredSize.V += lineSize.V; - return desiredSize.ToSize(); + //the last line size, if any should be added + panelSize.U = Max(curLineSize.U, panelSize.U); + panelSize.V += curLineSize.V; + + //go from UV space to W/H space + return new Size(panelSize.Width, panelSize.Height); } /// protected override Size ArrangeOverride(Size finalSize) { + int firstInLine = 0; double accumulatedV = 0; - var uvFinalSize = CreateUVSize(finalSize); - var lineSize = CreateUVSize(); - int firstChildInLineIndex = 0; - for (int index = 0; index < Children.Count; index++) + UVSize curLineSize = new UVSize(Orientation); + UVSize uvFinalSize = new UVSize(Orientation, finalSize.Width, finalSize.Height); + + for (int i = 0; i < Children.Count; i++) { - var child = Children[index]; - var childSize = CreateUVSize(child.DesiredSize); - if (lineSize.U + childSize.U <= uvFinalSize.U) // same line + var child = Children[i]; + if (child == null) continue; + + var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height); + + if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvFinalSize.U)) //need to switch to another line { - lineSize.U += childSize.U; - lineSize.V = Max(lineSize.V, childSize.V); + arrangeLine(accumulatedV, curLineSize.V, firstInLine, i); + + accumulatedV += curLineSize.V; + curLineSize = sz; + + if (MathUtilities.GreaterThan(sz.U, uvFinalSize.U)) //the element is wider then the constraint - give it a separate line + { + //switch to next line which only contain one element + arrangeLine(accumulatedV, sz.V, i, ++i); + + accumulatedV += sz.V; + curLineSize = new UVSize(Orientation); + } + firstInLine = i; } - else // moving to next line + else //continue to accumulate a line { - var controlsInLine = GetControlsBetween(firstChildInLineIndex, index); - ArrangeLine(accumulatedV, lineSize.V, controlsInLine); - accumulatedV += lineSize.V; - lineSize = childSize; - firstChildInLineIndex = index; + curLineSize.U += sz.U; + curLineSize.V = Max(sz.V, curLineSize.V); } } - if (firstChildInLineIndex < Children.Count) + //arrange the last line, if any + if (firstInLine < Children.Count) { - var controlsInLine = GetControlsBetween(firstChildInLineIndex, Children.Count); - ArrangeLine(accumulatedV, lineSize.V, controlsInLine); + arrangeLine(accumulatedV, curLineSize.V, firstInLine, Children.Count); } - return finalSize; - } - private IEnumerable GetControlsBetween(int first, int last) - { - return Children.Skip(first).Take(last - first); + return finalSize; } - private void ArrangeLine(double v, double lineV, IEnumerable controls) + private void arrangeLine(double v, double lineV, int start, int end) { double u = 0; bool isHorizontal = (Orientation == Orientation.Horizontal); - foreach (var child in controls) + + for (int i = start; i < end; i++) { - var childSize = CreateUVSize(child.DesiredSize); - var x = isHorizontal ? u : v; - var y = isHorizontal ? v : u; - var width = isHorizontal ? childSize.U : lineV; - var height = isHorizontal ? lineV : childSize.U; - child.Arrange(new Rect(x, y, width, height)); - u += childSize.U; + var child = Children[i]; + if (child != null) + { + UVSize childSize = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height); + double layoutSlotU = childSize.U; + child.Arrange(new Rect( + (isHorizontal ? u : v), + (isHorizontal ? v : u), + (isHorizontal ? layoutSlotU : lineV), + (isHorizontal ? lineV : layoutSlotU))); + u += layoutSlotU; + } } } - /// - /// Used to not not write separate code for horizontal and vertical orientation. - /// U is direction in line. (x if orientation is horizontal) - /// V is direction of lines. (y if orientation is horizontal) - /// - [DebuggerDisplay("U = {U} V = {V}")] + private struct UVSize { - private readonly Orientation _orientation; - - internal double U; - - internal double V; - - private UVSize(Orientation orientation, double width, double height) + internal UVSize(Orientation orientation, double width, double height) { U = V = 0d; _orientation = orientation; @@ -202,52 +221,25 @@ namespace Avalonia.Controls Height = height; } - internal UVSize(Orientation orientation, Size size) - : this(orientation, size.Width, size.Height) - { - } - internal UVSize(Orientation orientation) { U = V = 0d; _orientation = orientation; } - private double Width + internal double U; + internal double V; + private Orientation _orientation; + + internal double Width { get { return (_orientation == Orientation.Horizontal ? U : V); } - set - { - if (_orientation == Orientation.Horizontal) - { - U = value; - } - else - { - V = value; - } - } + set { if (_orientation == Orientation.Horizontal) U = value; else V = value; } } - - private double Height + internal double Height { get { return (_orientation == Orientation.Horizontal ? V : U); } - set - { - if (_orientation == Orientation.Horizontal) - { - V = value; - } - else - { - U = value; - } - } - } - - public Size ToSize() - { - return new Size(Width, Height); + set { if (_orientation == Orientation.Horizontal) V = value; else U = value; } } } } diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 801ed74ca0..a0e86a53b0 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -9,7 +9,6 @@ using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Designer; using Avalonia.Remote.Protocol.Viewport; using Avalonia.Threading; -using Portable.Xaml; namespace Avalonia.DesignerSupport.Remote { @@ -206,7 +205,6 @@ namespace Avalonia.DesignerSupport.Remote } catch (Exception e) { - var xamlException = e as XamlException; var xmlException = e as XmlException; s_transport.Send(new UpdateXamlResultMessage @@ -216,8 +214,8 @@ namespace Avalonia.DesignerSupport.Remote { ExceptionType = e.GetType().FullName, Message = e.Message.ToString(), - LineNumber = xamlException?.LineNumber ?? xmlException?.LineNumber, - LinePosition = xamlException?.LinePosition ?? xmlException?.LinePosition, + LineNumber = xmlException?.LineNumber, + LinePosition = xmlException?.LinePosition, } }); } diff --git a/src/Avalonia.Desktop/Avalonia.Desktop.csproj b/src/Avalonia.Desktop/Avalonia.Desktop.csproj index f7c06306f5..f45b0c54e3 100644 --- a/src/Avalonia.Desktop/Avalonia.Desktop.csproj +++ b/src/Avalonia.Desktop/Avalonia.Desktop.csproj @@ -1,6 +1,7 @@ netstandard2.0 + Avalonia.Desktop diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs index d3618f30f3..8139af1659 100644 --- a/src/Avalonia.Input/Cursors.cs +++ b/src/Avalonia.Input/Cursors.cs @@ -38,6 +38,7 @@ namespace Avalonia.Input DragMove, DragCopy, DragLink, + None, // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 28fb26dca4..80e18cb7bf 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -146,7 +146,7 @@ namespace Avalonia.Input /// /// The element. /// True if the element can be focused. - private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible; + private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && e.IsVisible; /// /// Gets the focus scope ancestors of the specified control, traversing popups. diff --git a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs new file mode 100644 index 0000000000..91b224e65a --- /dev/null +++ b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.LogicalTree; +using Avalonia.Styling; + +namespace Avalonia.Input.GestureRecognizers +{ + public class GestureRecognizerCollection : IReadOnlyCollection, IGestureRecognizerActionsDispatcher + { + private readonly IInputElement _inputElement; + private List _recognizers; + private Dictionary _pointerGrabs; + + + public GestureRecognizerCollection(IInputElement inputElement) + { + _inputElement = inputElement; + } + + public void Add(IGestureRecognizer recognizer) + { + if (_recognizers == null) + { + // We initialize the collection when the first recognizer is added + _recognizers = new List(); + _pointerGrabs = new Dictionary(); + } + + _recognizers.Add(recognizer); + recognizer.Initialize(_inputElement, this); + + // Hacks to make bindings work + + if (_inputElement is ILogical logicalParent && recognizer is ISetLogicalParent logical) + { + logical.SetParent(logicalParent); + if (recognizer is IStyleable styleableRecognizer + && _inputElement is IStyleable styleableParent) + styleableRecognizer.Bind(StyledElement.TemplatedParentProperty, + styleableParent.GetObservable(StyledElement.TemplatedParentProperty)); + } + } + + static readonly List s_Empty = new List(); + + public IEnumerator GetEnumerator() + => _recognizers?.GetEnumerator() ?? s_Empty.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => _recognizers?.Count ?? 0; + + + internal bool HandlePointerPressed(PointerPressedEventArgs e) + { + if (_recognizers == null) + return false; + foreach (var r in _recognizers) + { + if(e.Handled) + break; + r.PointerPressed(e); + } + + return e.Handled; + } + + internal bool HandlePointerReleased(PointerReleasedEventArgs e) + { + if (_recognizers == null) + return false; + if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) + { + capture.PointerReleased(e); + } + else + foreach (var r in _recognizers) + { + if (e.Handled) + break; + r.PointerReleased(e); + } + return e.Handled; + } + + internal bool HandlePointerMoved(PointerEventArgs e) + { + if (_recognizers == null) + return false; + if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) + { + capture.PointerMoved(e); + } + else + foreach (var r in _recognizers) + { + if (e.Handled) + break; + r.PointerMoved(e); + } + return e.Handled; + } + + internal void HandlePointerCaptureLost(PointerCaptureLostEventArgs e) + { + if (_recognizers == null) + return; + _pointerGrabs.Remove(e.Pointer); + foreach (var r in _recognizers) + { + if(e.Handled) + break; + r.PointerCaptureLost(e); + } + } + + void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer) + { + pointer.Capture(_inputElement); + _pointerGrabs[pointer] = recognizer; + } + + } +} diff --git a/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs new file mode 100644 index 0000000000..b8ba9e529c --- /dev/null +++ b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Input.GestureRecognizers +{ + public interface IGestureRecognizer + { + void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions); + void PointerPressed(PointerPressedEventArgs e); + void PointerReleased(PointerReleasedEventArgs e); + void PointerMoved(PointerEventArgs e); + void PointerCaptureLost(PointerCaptureLostEventArgs e); + } + + public interface IGestureRecognizerActionsDispatcher + { + void Capture(IPointer pointer, IGestureRecognizer recognizer); + } + + public enum GestureRecognizerResult + { + None, + Capture, + ReleaseCapture + } +} diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs new file mode 100644 index 0000000000..4f3c7c0bba --- /dev/null +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -0,0 +1,183 @@ +using System; +using System.Diagnostics; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace Avalonia.Input.GestureRecognizers +{ + public class ScrollGestureRecognizer + : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise + IGestureRecognizer + { + private bool _scrolling; + private Point _trackedRootPoint; + private IPointer _tracking; + private IInputElement _target; + private IGestureRecognizerActionsDispatcher _actions; + private bool _canHorizontallyScroll; + private bool _canVerticallyScroll; + private int _gestureId; + + // Movement per second + private Vector _inertia; + private ulong? _lastMoveTimestamp; + + /// + /// Defines the property. + /// + public static readonly DirectProperty CanHorizontallyScrollProperty = + AvaloniaProperty.RegisterDirect( + nameof(CanHorizontallyScroll), + o => o.CanHorizontallyScroll, + (o, v) => o.CanHorizontallyScroll = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CanVerticallyScrollProperty = + AvaloniaProperty.RegisterDirect( + nameof(CanVerticallyScroll), + o => o.CanVerticallyScroll, + (o, v) => o.CanVerticallyScroll = v); + + /// + /// Gets or sets a value indicating whether the content can be scrolled horizontally. + /// + public bool CanHorizontallyScroll + { + get => _canHorizontallyScroll; + set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); + } + + /// + /// Gets or sets a value indicating whether the content can be scrolled horizontally. + /// + public bool CanVerticallyScroll + { + get => _canVerticallyScroll; + set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); + } + + + public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) + { + _target = target; + _actions = actions; + } + + public void PointerPressed(PointerPressedEventArgs e) + { + if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch) + { + EndGesture(); + _tracking = e.Pointer; + _gestureId = ScrollGestureEventArgs.GetNextFreeId();; + _trackedRootPoint = e.GetPosition(null); + } + } + + // Arbitrary chosen value, probably need to move that to platform settings or something + private const double ScrollStartDistance = 30; + + // Pixels per second speed that is considered to be the stop of inertiall scroll + private const double InertialScrollSpeedEnd = 5; + + public void PointerMoved(PointerEventArgs e) + { + if (e.Pointer == _tracking) + { + var rootPoint = e.GetPosition(null); + if (!_scrolling) + { + if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance) + _scrolling = true; + if (CanVerticallyScroll && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > ScrollStartDistance) + _scrolling = true; + if (_scrolling) + { + _actions.Capture(e.Pointer, this); + } + } + + if (_scrolling) + { + var vector = _trackedRootPoint - rootPoint; + var elapsed = _lastMoveTimestamp.HasValue ? + TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) : + TimeSpan.Zero; + + _lastMoveTimestamp = e.Timestamp; + _trackedRootPoint = rootPoint; + if (elapsed.TotalSeconds > 0) + _inertia = vector / elapsed.TotalSeconds; + _target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); + e.Handled = true; + } + } + } + + public void PointerCaptureLost(PointerCaptureLostEventArgs e) + { + if (e.Pointer == _tracking) EndGesture(); + } + + void EndGesture() + { + _tracking = null; + if (_scrolling) + { + _inertia = default; + _scrolling = false; + _target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); + _gestureId = 0; + _lastMoveTimestamp = null; + } + + } + + + public void PointerReleased(PointerReleasedEventArgs e) + { + if (e.Pointer == _tracking && _scrolling) + { + e.Handled = true; + if (_inertia == default + || e.Timestamp == 0 + || _lastMoveTimestamp == 0 + || e.Timestamp - _lastMoveTimestamp > 200) + EndGesture(); + else + { + var savedGestureId = _gestureId; + var st = Stopwatch.StartNew(); + var lastTime = TimeSpan.Zero; + DispatcherTimer.Run(() => + { + // Another gesture has started, finish the current one + if (_gestureId != savedGestureId) + { + return false; + } + + var elapsedSinceLastTick = st.Elapsed - lastTime; + lastTime = st.Elapsed; + + var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); + var distance = speed * elapsedSinceLastTick.TotalSeconds; + _target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); + + + + if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd) + { + EndGesture(); + return false; + } + + return true; + }, TimeSpan.FromMilliseconds(16), DispatcherPriority.Background); + } + } + } + } +} diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 8a514ca685..65195394ab 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -18,6 +18,14 @@ namespace Avalonia.Input RoutingStrategies.Bubble, typeof(Gestures)); + public static readonly RoutedEvent ScrollGestureEvent = + RoutedEvent.Register( + "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent ScrollGestureEndedEvent = + RoutedEvent.Register( + "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); + private static WeakReference s_lastPress; static Gestures() @@ -38,7 +46,10 @@ namespace Avalonia.Input } else if (s_lastPress?.IsAlive == true && e.ClickCount == 2 && s_lastPress.Target == e.Source) { - e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); + if (!ev.Handled) + { + e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); + } } } } @@ -51,7 +62,10 @@ namespace Avalonia.Input if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source) { - ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent)); + if (!ev.Handled) + { + ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent)); + } } } } diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index c9924dbffb..9247fb48a9 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -83,14 +83,14 @@ namespace Avalonia.Input Cursor Cursor { get; } /// - /// Gets a value indicating whether the control is effectively enabled for user interaction. + /// Gets a value indicating whether this control and all its parents are enabled. /// /// /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the + /// controls. The property takes into account the /// value of this control and its parent controls. /// - bool IsEnabledCore { get; } + bool IsEffectivelyEnabled { get; } /// /// Gets a value indicating whether the control is focused. diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs index a1d1bb3eb8..7e6bf657ae 100644 --- a/src/Avalonia.Input/IMouseDevice.cs +++ b/src/Avalonia.Input/IMouseDevice.cs @@ -1,6 +1,8 @@ // 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; + namespace Avalonia.Input { /// @@ -11,6 +13,9 @@ namespace Avalonia.Input /// /// Gets the mouse position, in screen coordinates. /// + [Obsolete("Use PointerEventArgs.GetPosition")] PixelPoint Position { get; } + + void SceneInvalidated(IInputRoot root, Rect rect); } } diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs new file mode 100644 index 0000000000..a3f051ce7f --- /dev/null +++ b/src/Avalonia.Input/IPointer.cs @@ -0,0 +1,18 @@ +namespace Avalonia.Input +{ + public interface IPointer + { + int Id { get; } + void Capture(IInputElement control); + IInputElement Captured { get; } + PointerType Type { get; } + bool IsPrimary { get; } + + } + + public enum PointerType + { + Mouse, + Touch + } +} diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs index 932cbc989f..de775d90f2 100644 --- a/src/Avalonia.Input/IPointerDevice.cs +++ b/src/Avalonia.Input/IPointerDevice.cs @@ -1,18 +1,20 @@ // 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.VisualTree; namespace Avalonia.Input { public interface IPointerDevice : IInputDevice { + [Obsolete("Use IPointer")] IInputElement Captured { get; } - + + [Obsolete("Use IPointer")] void Capture(IInputElement control); + [Obsolete("Use PointerEventArgs.GetPosition")] Point GetPosition(IVisual relativeTo); - - void SceneInvalidated(IInputRoot root, Rect rect); } } diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 07e04486ec..4b4ab177b8 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Input.GestureRecognizers; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -27,10 +28,12 @@ namespace Avalonia.Input AvaloniaProperty.Register(nameof(IsEnabled), true); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty IsEnabledCoreProperty = - AvaloniaProperty.Register(nameof(IsEnabledCore), true); + public static readonly DirectProperty IsEffectivelyEnabledProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsEffectivelyEnabled), + o => o.IsEffectivelyEnabled); /// /// Gets or sets associated mouse cursor. @@ -127,6 +130,14 @@ namespace Avalonia.Input RoutedEvent.Register( "PointerReleased", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent PointerCaptureLostEvent = + RoutedEvent.Register( + "PointerCaptureLost", + RoutingStrategies.Direct); /// /// Defines the event. @@ -146,8 +157,10 @@ namespace Avalonia.Input /// public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; + private bool _isEffectivelyEnabled = true; private bool _isFocused; private bool _isPointerOver; + private GestureRecognizerCollection _gestureRecognizers; /// /// Initializes static members of the class. @@ -166,9 +179,10 @@ namespace Avalonia.Input PointerMovedEvent.AddClassHandler(x => x.OnPointerMoved); PointerPressedEvent.AddClassHandler(x => x.OnPointerPressed); PointerReleasedEvent.AddClassHandler(x => x.OnPointerReleased); + PointerCaptureLostEvent.AddClassHandler(x => x.OnPointerCaptureLost); PointerWheelChangedEvent.AddClassHandler(x => x.OnPointerWheelChanged); - PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled"); + PseudoClass(IsEffectivelyEnabledProperty, x => !x, ":disabled"); PseudoClass(IsFocusedProperty, ":focus"); PseudoClass(IsPointerOverProperty, ":pointerover"); } @@ -263,6 +277,16 @@ namespace Avalonia.Input remove { RemoveHandler(PointerReleasedEvent, value); } } + /// + /// Occurs when the control or its child control loses the pointer capture for any reason, + /// event will not be triggered for a parent control if capture was transferred to another child of that parent control + /// + public event EventHandler PointerCaptureLost + { + add => AddHandler(PointerCaptureLostEvent, value); + remove => RemoveHandler(PointerCaptureLostEvent, value); + } + /// /// Occurs when the mouse wheen is scrolled over the control. /// @@ -344,31 +368,28 @@ namespace Avalonia.Input internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); } } - /// - /// Gets a value indicating whether the control is effectively enabled for user interaction. - /// - /// - /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the - /// value of this control and its parent controls. - /// - bool IInputElement.IsEnabledCore => IsEnabledCore; + /// + public bool IsEffectivelyEnabled + { + get => _isEffectivelyEnabled; + private set => SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); + } + + public List KeyBindings { get; } = new List(); /// - /// Gets a value indicating whether the control is effectively enabled for user interaction. + /// Allows a derived class to override the enabled state of the control. /// /// - /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the - /// value of this control and its parent controls. + /// Derived controls may wish to disable the enabled state of the control without overwriting the + /// user-supplied setting. This can be done by overriding this property + /// to return the overridden enabled state. If the value returned from + /// should change, then the derived control should call . /// - protected bool IsEnabledCore - { - get { return GetValue(IsEnabledCoreProperty); } - set { SetValue(IsEnabledCoreProperty, value); } - } + protected virtual bool IsEnabledCore => IsEnabled; - public List KeyBindings { get; } = new List(); + public GestureRecognizerCollection GestureRecognizers + => _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this)); /// /// Focuses the control. @@ -393,7 +414,7 @@ namespace Avalonia.Input protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTreeCore(e); - UpdateIsEnabledCore(); + UpdateIsEffectivelyEnabled(); } /// @@ -460,6 +481,8 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerMoved(PointerEventArgs e) { + if (_gestureRecognizers?.HandlePointerMoved(e) == true) + e.Handled = true; } /// @@ -468,6 +491,8 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerPressed(PointerPressedEventArgs e) { + if (_gestureRecognizers?.HandlePointerPressed(e) == true) + e.Handled = true; } /// @@ -476,6 +501,17 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerReleased(PointerReleasedEventArgs e) { + if (_gestureRecognizers?.HandlePointerReleased(e) == true) + e.Handled = true; + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + _gestureRecognizers?.HandlePointerCaptureLost(e); } /// @@ -486,9 +522,18 @@ namespace Avalonia.Input { } + /// + /// Updates the property value according to the parent + /// control's enabled state and . + /// + protected void UpdateIsEffectivelyEnabled() + { + UpdateIsEffectivelyEnabled(this.GetVisualParent()); + } + private static void IsEnabledChanged(AvaloniaPropertyChangedEventArgs e) { - ((InputElement)e.Sender).UpdateIsEnabledCore(); + ((InputElement)e.Sender).UpdateIsEffectivelyEnabled(); } /// @@ -512,32 +557,17 @@ namespace Avalonia.Input } /// - /// Updates the property value. - /// - private void UpdateIsEnabledCore() - { - UpdateIsEnabledCore(this.GetVisualParent()); - } - - /// - /// Updates the property based on the parent's - /// . + /// Updates the property based on the parent's + /// . /// /// The parent control. - private void UpdateIsEnabledCore(InputElement parent) + private void UpdateIsEffectivelyEnabled(InputElement parent) { - if (parent != null) - { - IsEnabledCore = IsEnabled && parent.IsEnabledCore; - } - else - { - IsEnabledCore = IsEnabled; - } + IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true); foreach (var child in this.GetVisualChildren().OfType()) { - child.UpdateIsEnabledCore(this); + child.UpdateIsEffectivelyEnabled(this); } } } diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index f184e41998..c1d0729560 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -45,7 +45,7 @@ namespace Avalonia.Input return element != null && element.IsVisible && element.IsHitTestVisible && - element.IsEnabledCore && + element.IsEffectivelyEnabled && element.IsAttachedToVisualTree; } } diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index d3e62ece6f..ee7d0c9501 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -19,9 +19,14 @@ namespace Avalonia.Input private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; - private IInputElement _captured; - private IDisposable _capturedSubscription; - + + private readonly Pointer _pointer; + + public MouseDevice(Pointer pointer = null) + { + _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + } + /// /// Gets the control that is currently capturing by the mouse, if any. /// @@ -30,27 +35,9 @@ namespace Avalonia.Input /// within the control's bounds or not. To set the mouse capture, call the /// method. /// - public IInputElement Captured - { - get => _captured; - protected set - { - _capturedSubscription?.Dispose(); - _capturedSubscription = null; + [Obsolete("Use IPointer instead")] + public IInputElement Captured => _pointer.Captured; - if (value != null) - { - _capturedSubscription = Observable.FromEventPattern( - x => value.DetachedFromVisualTree += x, - x => value.DetachedFromVisualTree -= x) - .Take(1) - .Subscribe(_ => Captured = null); - } - - _captured = value; - } - } - /// /// Gets the mouse position, in screen coordinates. /// @@ -69,10 +56,9 @@ namespace Avalonia.Input /// within the control's bounds or not. The current mouse capture control is exposed /// by the property. /// - public virtual void Capture(IInputElement control) + public void Capture(IInputElement control) { - // TODO: Check visibility and enabled state before setting capture. - Captured = control; + _pointer.Capture(control); } /// @@ -96,7 +82,7 @@ namespace Avalonia.Input public void ProcessRawEvent(RawInputEventArgs e) { - if (!e.Handled && e is RawMouseEventArgs margs) + if (!e.Handled && e is RawPointerEventArgs margs) ProcessRawEvent(margs); } @@ -106,66 +92,100 @@ namespace Avalonia.Input if (rect.Contains(clientPoint)) { - if (Captured == null) + if (_pointer.Captured == null) { - SetPointerOver(this, root, clientPoint); + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, InputModifiers.None); } else { - SetPointerOver(this, root, Captured); + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, InputModifiers.None); } } } - private void ProcessRawEvent(RawMouseEventArgs e) + int ButtonCount(PointerPointProperties props) + { + var rv = 0; + if (props.IsLeftButtonPressed) + rv++; + if (props.IsMiddleButtonPressed) + rv++; + if (props.IsRightButtonPressed) + rv++; + return rv; + } + + private void ProcessRawEvent(RawPointerEventArgs e) { Contract.Requires(e != null); var mouse = (IMouseDevice)e.Device; Position = e.Root.PointToScreen(e.Position); - + var props = CreateProperties(e); switch (e.Type) { - case RawMouseEventType.LeaveWindow: - LeaveWindow(mouse, e.Root); + case RawPointerEventType.LeaveWindow: + LeaveWindow(mouse, e.Timestamp, e.Root, e.InputModifiers); break; - case RawMouseEventType.LeftButtonDown: - case RawMouseEventType.RightButtonDown: - case RawMouseEventType.MiddleButtonDown: - e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - e.Type == RawMouseEventType.LeftButtonDown - ? MouseButton.Left - : e.Type == RawMouseEventType.RightButtonDown ? MouseButton.Right : MouseButton.Middle, - e.InputModifiers); + case RawPointerEventType.LeftButtonDown: + case RawPointerEventType.RightButtonDown: + case RawPointerEventType.MiddleButtonDown: + if (ButtonCount(props) > 1) + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); + else + e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, + props, e.InputModifiers); break; - case RawMouseEventType.LeftButtonUp: - case RawMouseEventType.RightButtonUp: - case RawMouseEventType.MiddleButtonUp: - e.Handled = MouseUp(mouse, e.Root, e.Position, - e.Type == RawMouseEventType.LeftButtonUp - ? MouseButton.Left - : e.Type == RawMouseEventType.RightButtonUp ? MouseButton.Right : MouseButton.Middle, - e.InputModifiers); + case RawPointerEventType.LeftButtonUp: + case RawPointerEventType.RightButtonUp: + case RawPointerEventType.MiddleButtonUp: + if (ButtonCount(props) != 0) + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); + else + e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); break; - case RawMouseEventType.Move: - e.Handled = MouseMove(mouse, e.Root, e.Position, e.InputModifiers); + case RawPointerEventType.Move: + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); break; - case RawMouseEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Root, e.Position, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers); + case RawPointerEventType.Wheel: + e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers); break; } } - private void LeaveWindow(IMouseDevice device, IInputRoot root) + private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); - ClearPointerOver(this, root); + ClearPointerOver(this, timestamp, root, inputModifiers); } - private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, MouseButton button, InputModifiers inputModifiers) + + PointerPointProperties CreateProperties(RawPointerEventArgs args) + { + var rv = new PointerPointProperties(args.InputModifiers); + + if (args.Type == RawPointerEventType.LeftButtonDown) + rv.IsLeftButtonPressed = true; + if (args.Type == RawPointerEventType.MiddleButtonDown) + rv.IsMiddleButtonPressed = true; + if (args.Type == RawPointerEventType.RightButtonDown) + rv.IsRightButtonPressed = true; + if (args.Type == RawPointerEventType.LeftButtonUp) + rv.IsLeftButtonPressed = false; + if (args.Type == RawPointerEventType.MiddleButtonUp) + rv.IsMiddleButtonPressed = false; + if (args.Type == RawPointerEventType.RightButtonUp) + rv.IsRightButtonPressed = false; + return rv; + } + + private MouseButton _lastMouseDownButton; + private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, + PointerPointProperties properties, + InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -174,8 +194,8 @@ namespace Avalonia.Input if (hit != null) { - IInteractive source = GetSource(hit); - + _pointer.Capture(hit); + var source = GetSource(hit); if (source != null) { var settings = AvaloniaLocator.Current.GetService(); @@ -190,17 +210,8 @@ namespace Avalonia.Input _lastClickTime = timestamp; _lastClickRect = new Rect(p, new Size()) .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2)); - - var e = new PointerPressedEventArgs - { - Device = this, - RoutedEvent = InputElement.PointerPressedEvent, - Source = source, - ClickCount = _clickCount, - MouseButton = button, - InputModifiers = inputModifiers - }; - + _lastMouseDownButton = properties.GetObsoleteMouseButton(); + var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); source.RaiseEvent(e); return e.Handled; } @@ -209,36 +220,33 @@ namespace Avalonia.Input return false; } - private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, InputModifiers inputModifiers) + private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, + InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); IInputElement source; - if (Captured == null) + if (_pointer.Captured == null) { - source = SetPointerOver(this, root, p); + source = SetPointerOver(this, timestamp, root, p, inputModifiers); } else { - SetPointerOver(this, root, Captured); - source = Captured; + SetPointerOver(this, timestamp, root, _pointer.Captured, inputModifiers); + source = _pointer.Captured; } - var e = new PointerEventArgs - { - Device = this, - RoutedEvent = InputElement.PointerMovedEvent, - Source = source, - InputModifiers = inputModifiers - }; + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, + p, timestamp, properties, inputModifiers); source?.RaiseEvent(e); return e.Handled; } - private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, MouseButton button, InputModifiers inputModifiers) + private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, + InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -248,23 +256,20 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerReleasedEventArgs - { - Device = this, - RoutedEvent = InputElement.PointerReleasedEvent, - Source = source, - MouseButton = button, - InputModifiers = inputModifiers - }; + var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, + _lastMouseDownButton); source?.RaiseEvent(e); + _pointer.Capture(null); return e.Handled; } return false; } - private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p, Vector delta, InputModifiers inputModifiers) + private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -274,14 +279,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerWheelEventArgs - { - Device = this, - RoutedEvent = InputElement.PointerWheelChangedEvent, - Source = source, - Delta = delta, - InputModifiers = inputModifiers - }; + var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; @@ -294,7 +292,7 @@ namespace Avalonia.Input { Contract.Requires(hit != null); - return Captured ?? + return _pointer.Captured ?? (hit as IInteractive) ?? hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); } @@ -303,20 +301,22 @@ namespace Avalonia.Input { Contract.Requires(root != null); - return Captured ?? root.InputHitTest(p); + return _pointer.Captured ?? root.InputHitTest(p); } - private void ClearPointerOver(IPointerDevice device, IInputRoot root) + PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, InputModifiers inputModifiers) + { + return new PointerEventArgs(ev, source, _pointer, null, default, + timestamp, new PointerPointProperties(inputModifiers), inputModifiers); + } + + private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var element = root.PointerOverElement; - var e = new PointerEventArgs - { - RoutedEvent = InputElement.PointerLeaveEvent, - Device = device, - }; + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, inputModifiers); if (element!=null && !element.IsAttachedToVisualTree) { @@ -353,7 +353,7 @@ namespace Avalonia.Input } } - private IInputElement SetPointerOver(IPointerDevice device, IInputRoot root, Point p) + private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -364,18 +364,18 @@ namespace Avalonia.Input { if (element != null) { - SetPointerOver(device, root, element); + SetPointerOver(device, timestamp, root, element, inputModifiers); } else { - ClearPointerOver(device, root); + ClearPointerOver(device, timestamp, root, inputModifiers); } } return element; } - private void SetPointerOver(IPointerDevice device, IInputRoot root, IInputElement element) + private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -383,7 +383,6 @@ namespace Avalonia.Input IInputElement branch = null; - var e = new PointerEventArgs { Device = device, }; var el = element; while (el != null) @@ -397,8 +396,8 @@ namespace Avalonia.Input } el = root.PointerOverElement; - e.RoutedEvent = InputElement.PointerLeaveEvent; + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, inputModifiers); if (el!=null && branch!=null && !el.IsAttachedToVisualTree) { ClearChildrenPointerOver(e,branch,false); diff --git a/src/Avalonia.Input/Navigation/FocusExtensions.cs b/src/Avalonia.Input/Navigation/FocusExtensions.cs index 41e7c4cd7b..794dc63f84 100644 --- a/src/Avalonia.Input/Navigation/FocusExtensions.cs +++ b/src/Avalonia.Input/Navigation/FocusExtensions.cs @@ -13,13 +13,13 @@ namespace Avalonia.Input.Navigation /// /// The element. /// True if the element can be focused. - public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible; + public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && e.IsVisible; /// /// Checks if descendants of the specified element can be focused. /// /// The element. /// True if descendants of the element can be focused. - public static bool CanFocusDescendants(this IInputElement e) => e.IsEnabledCore && e.IsVisible; + public static bool CanFocusDescendants(this IInputElement e) => e.IsEffectivelyEnabled && e.IsVisible; } } diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs new file mode 100644 index 0000000000..80d803abb1 --- /dev/null +++ b/src/Avalonia.Input/Pointer.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public class Pointer : IPointer, IDisposable + { + private static int s_NextFreePointerId = 1000; + public static int GetNextFreeId() => s_NextFreePointerId++; + + public Pointer(int id, PointerType type, bool isPrimary) + { + Id = id; + Type = type; + IsPrimary = isPrimary; + } + + public int Id { get; } + + IInputElement FindCommonParent(IInputElement control1, IInputElement control2) + { + if (control1 == null || control2 == null) + return null; + var seen = new HashSet(control1.GetSelfAndVisualAncestors().OfType()); + return control2.GetSelfAndVisualAncestors().OfType().FirstOrDefault(seen.Contains); + } + + protected virtual void PlatformCapture(IInputElement element) + { + + } + + public void Capture(IInputElement control) + { + if (Captured != null) + Captured.DetachedFromVisualTree -= OnCaptureDetached; + var oldCapture = control; + Captured = control; + PlatformCapture(control); + if (oldCapture != null) + { + var commonParent = FindCommonParent(control, oldCapture); + foreach (var notifyTarget in oldCapture.GetSelfAndVisualAncestors().OfType()) + { + if (notifyTarget == commonParent) + break; + notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this)); + } + } + + if (Captured != null) + Captured.DetachedFromVisualTree += OnCaptureDetached; + } + + IInputElement GetNextCapture(IVisual parent) => + parent as IInputElement ?? parent.GetVisualAncestors().OfType().FirstOrDefault(); + + private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e) + { + Capture(GetNextCapture(e.Parent)); + } + + + public IInputElement Captured { get; private set; } + + public PointerType Type { get; } + public bool IsPrimary { get; } + public void Dispose() => Capture(null); + } +} diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 7b2497c460..c827822192 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -1,6 +1,8 @@ // 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.Input.Raw; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -8,25 +10,69 @@ namespace Avalonia.Input { public class PointerEventArgs : RoutedEventArgs { - public PointerEventArgs() - { + private readonly IVisual _rootVisual; + private readonly Point _rootVisualPosition; + private readonly PointerPointProperties _properties; + public PointerEventArgs(RoutedEvent routedEvent, + IInteractive source, + IPointer pointer, + IVisual rootVisual, Point rootVisualPosition, + ulong timestamp, + PointerPointProperties properties, + InputModifiers modifiers) + : base(routedEvent) + { + Source = source; + _rootVisual = rootVisual; + _rootVisualPosition = rootVisualPosition; + _properties = properties; + Pointer = pointer; + Timestamp = timestamp; + InputModifiers = modifiers; } - public PointerEventArgs(RoutedEvent routedEvent) - : base(routedEvent) + class EmulatedDevice : IPointerDevice { + private readonly PointerEventArgs _ev; + + public EmulatedDevice(PointerEventArgs ev) + { + _ev = ev; + } + + public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException(); + public IInputElement Captured => _ev.Pointer.Captured; + public void Capture(IInputElement control) + { + _ev.Pointer.Capture(control); + } + + public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo); } - public IPointerDevice Device { get; set; } + public IPointer Pointer { get; } + public ulong Timestamp { get; } + + private IPointerDevice _device; - public InputModifiers InputModifiers { get; set; } + [Obsolete("Use Pointer to get pointer-specific information")] + public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this)); + + public InputModifiers InputModifiers { get; } public Point GetPosition(IVisual relativeTo) { - return Device.GetPosition(relativeTo); + if (_rootVisual == null) + return default; + if (relativeTo == null) + return _rootVisualPosition; + return _rootVisualPosition * _rootVisual.TransformToVisual(relativeTo) ?? default; } + + public PointerPoint GetPointerPoint(IVisual relativeTo) + => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); } public enum MouseButton @@ -39,32 +85,52 @@ namespace Avalonia.Input public class PointerPressedEventArgs : PointerEventArgs { - public PointerPressedEventArgs() - : base(InputElement.PointerPressedEvent) - { - } + private readonly int _obsoleteClickCount; - public PointerPressedEventArgs(RoutedEvent routedEvent) - : base(routedEvent) + public PointerPressedEventArgs( + IInteractive source, + IPointer pointer, + IVisual rootVisual, Point rootVisualPosition, + ulong timestamp, + PointerPointProperties properties, + InputModifiers modifiers, + int obsoleteClickCount = 1) + : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) { + _obsoleteClickCount = obsoleteClickCount; } - public int ClickCount { get; set; } - public MouseButton MouseButton { get; set; } + [Obsolete("Use DoubleTapped or DoubleRightTapped event instead")] + public int ClickCount => _obsoleteClickCount; + + [Obsolete] public MouseButton MouseButton => GetPointerPoint(null).Properties.GetObsoleteMouseButton(); } public class PointerReleasedEventArgs : PointerEventArgs { - public PointerReleasedEventArgs() - : base(InputElement.PointerReleasedEvent) + public PointerReleasedEventArgs( + IInteractive source, IPointer pointer, + IVisual rootVisual, Point rootVisualPosition, ulong timestamp, + PointerPointProperties properties, InputModifiers modifiers, MouseButton obsoleteMouseButton) + : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) { + MouseButton = obsoleteMouseButton; } - public PointerReleasedEventArgs(RoutedEvent routedEvent) - : base(routedEvent) + [Obsolete()] + public MouseButton MouseButton { get; private set; } + } + + public class PointerCaptureLostEventArgs : RoutedEventArgs + { + public IPointer Pointer { get; } + + public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) { + Pointer = pointer; + Source = source; } - - public MouseButton MouseButton { get; set; } } } diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs new file mode 100644 index 0000000000..7117b5709c --- /dev/null +++ b/src/Avalonia.Input/PointerPoint.cs @@ -0,0 +1,45 @@ +namespace Avalonia.Input +{ + public sealed class PointerPoint + { + public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties) + { + Pointer = pointer; + Position = position; + Properties = properties; + } + public IPointer Pointer { get; } + public PointerPointProperties Properties { get; } + public Point Position { get; } + } + + public sealed class PointerPointProperties + { + public bool IsLeftButtonPressed { get; set; } + public bool IsMiddleButtonPressed { get; set; } + public bool IsRightButtonPressed { get; set; } + + public PointerPointProperties() + { + + } + + public PointerPointProperties(InputModifiers modifiers) + { + IsLeftButtonPressed = modifiers.HasFlag(InputModifiers.LeftMouseButton); + IsMiddleButtonPressed = modifiers.HasFlag(InputModifiers.MiddleMouseButton); + IsRightButtonPressed = modifiers.HasFlag(InputModifiers.RightMouseButton); + } + + public MouseButton GetObsoleteMouseButton() + { + if (IsLeftButtonPressed) + return MouseButton.Left; + if (IsMiddleButtonPressed) + return MouseButton.Middle; + if (IsRightButtonPressed) + return MouseButton.Right; + return MouseButton.None; + } + } +} diff --git a/src/Avalonia.Input/PointerWheelEventArgs.cs b/src/Avalonia.Input/PointerWheelEventArgs.cs index 401d1d847c..de1badfe96 100644 --- a/src/Avalonia.Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Input/PointerWheelEventArgs.cs @@ -1,10 +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. +using Avalonia.Interactivity; +using Avalonia.VisualTree; + namespace Avalonia.Input { public class PointerWheelEventArgs : PointerEventArgs { public Vector Delta { get; set; } + + public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + Point rootVisualPosition, ulong timestamp, + PointerPointProperties properties, InputModifiers modifiers, Vector delta) + : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) + { + Delta = delta; + } } } diff --git a/src/Avalonia.Input/Properties/AssemblyInfo.cs b/src/Avalonia.Input/Properties/AssemblyInfo.cs index 7025965f83..3a8d358931 100644 --- a/src/Avalonia.Input/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Input/Properties/AssemblyInfo.cs @@ -5,3 +5,4 @@ using System.Reflection; using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")] diff --git a/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs b/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs index d2e5faab6c..186ad99efc 100644 --- a/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs @@ -4,7 +4,7 @@ namespace Avalonia.Input.Raw { - public class RawMouseWheelEventArgs : RawMouseEventArgs + public class RawMouseWheelEventArgs : RawPointerEventArgs { public RawMouseWheelEventArgs( IInputDevice device, @@ -12,7 +12,7 @@ namespace Avalonia.Input.Raw IInputRoot root, Point position, Vector delta, InputModifiers inputModifiers) - : base(device, timestamp, root, RawMouseEventType.Wheel, position, inputModifiers) + : base(device, timestamp, root, RawPointerEventType.Wheel, position, inputModifiers) { Delta = delta; } diff --git a/src/Avalonia.Input/Raw/RawMouseEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs similarity index 84% rename from src/Avalonia.Input/Raw/RawMouseEventArgs.cs rename to src/Avalonia.Input/Raw/RawPointerEventArgs.cs index c5637d66cc..b728844e97 100644 --- a/src/Avalonia.Input/Raw/RawMouseEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -5,7 +5,7 @@ using System; namespace Avalonia.Input.Raw { - public enum RawMouseEventType + public enum RawPointerEventType { LeaveWindow, LeftButtonDown, @@ -17,15 +17,18 @@ namespace Avalonia.Input.Raw Move, Wheel, NonClientLeftButtonDown, + TouchBegin, + TouchUpdate, + TouchEnd } /// /// A raw mouse event. /// - public class RawMouseEventArgs : RawInputEventArgs + public class RawPointerEventArgs : RawInputEventArgs { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The associated device. /// The event timestamp. @@ -33,11 +36,11 @@ namespace Avalonia.Input.Raw /// The type of the event. /// The mouse position, in client DIPs. /// The input modifiers. - public RawMouseEventArgs( + public RawPointerEventArgs( IInputDevice device, ulong timestamp, IInputRoot root, - RawMouseEventType type, + RawPointerEventType type, Point position, InputModifiers inputModifiers) : base(device, timestamp) @@ -64,7 +67,7 @@ namespace Avalonia.Input.Raw /// /// Gets the type of the event. /// - public RawMouseEventType Type { get; private set; } + public RawPointerEventType Type { get; private set; } /// /// Gets the input modifiers. diff --git a/src/Avalonia.Input/Raw/RawTouchEventArgs.cs b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs new file mode 100644 index 0000000000..5299633b26 --- /dev/null +++ b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Input.Raw +{ + public class RawTouchEventArgs : RawPointerEventArgs + { + public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root, + RawPointerEventType type, Point position, InputModifiers inputModifiers, + long touchPointId) + : base(device, timestamp, root, type, position, inputModifiers) + { + TouchPointId = touchPointId; + } + + public long TouchPointId { get; set; } + } +} diff --git a/src/Avalonia.Input/ScrollGestureEventArgs.cs b/src/Avalonia.Input/ScrollGestureEventArgs.cs new file mode 100644 index 0000000000..a682e8f0a4 --- /dev/null +++ b/src/Avalonia.Input/ScrollGestureEventArgs.cs @@ -0,0 +1,29 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + public class ScrollGestureEventArgs : RoutedEventArgs + { + public int Id { get; } + public Vector Delta { get; } + private static int _nextId = 1; + + public static int GetNextFreeId() => _nextId++; + + public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) + { + Id = id; + Delta = delta; + } + } + + public class ScrollGestureEndedEventArgs : RoutedEventArgs + { + public int Id { get; } + + public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) + { + Id = id; + } + } +} diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs new file mode 100644 index 0000000000..7f473bb320 --- /dev/null +++ b/src/Avalonia.Input/TouchDevice.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Input.Raw; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + /// + /// Handles raw touch events + /// + /// This class is supposed to be used on per-toplevel basis, don't use a shared one + /// + /// + public class TouchDevice : IInputDevice + { + Dictionary _pointers = new Dictionary(); + + static InputModifiers GetModifiers(InputModifiers modifiers, bool left) + { + var mask = (InputModifiers)0x7fffffff ^ InputModifiers.LeftMouseButton ^ InputModifiers.MiddleMouseButton ^ + InputModifiers.RightMouseButton; + modifiers &= mask; + if (left) + modifiers |= InputModifiers.LeftMouseButton; + return modifiers; + } + + public void ProcessRawEvent(RawInputEventArgs ev) + { + var args = (RawTouchEventArgs)ev; + if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) + { + if (args.Type == RawPointerEventType.TouchEnd) + return; + var hit = args.Root.InputHitTest(args.Position); + + _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), + PointerType.Touch, _pointers.Count == 0); + pointer.Capture(hit); + } + + + var target = pointer.Captured ?? args.Root; + if (args.Type == RawPointerEventType.TouchBegin) + { + target.RaiseEvent(new PointerPressedEventArgs(target, pointer, + args.Root, args.Position, ev.Timestamp, + new PointerPointProperties(GetModifiers(args.InputModifiers, pointer.IsPrimary)), + GetModifiers(args.InputModifiers, false))); + } + + if (args.Type == RawPointerEventType.TouchEnd) + { + _pointers.Remove(args.TouchPointId); + using (pointer) + { + target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, + args.Root, args.Position, ev.Timestamp, + new PointerPointProperties(GetModifiers(args.InputModifiers, false)), + GetModifiers(args.InputModifiers, pointer.IsPrimary), + pointer.IsPrimary ? MouseButton.Left : MouseButton.None)); + } + } + + if (args.Type == RawPointerEventType.TouchUpdate) + { + var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); + target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, + args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers)); + } + } + + } +} diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index c8ee73ad5d..35e50b1b36 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -1,4 +1,4 @@ - + false @@ -7,8 +7,6 @@ /usr/bin/castxml /usr/local/bin/castxml true - - $(MSBuildThisFileDirectory)/Generated diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 73b8834375..638879ba14 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -226,7 +226,7 @@ namespace Avalonia.Native break; default: - Input?.Invoke(new RawMouseEventArgs(_mouse, timeStamp, _inputRoot, (RawMouseEventType)type, point.ToAvaloniaPoint(), (InputModifiers)modifiers)); + Input?.Invoke(new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (InputModifiers)modifiers)); break; } } diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs new file mode 100644 index 0000000000..4b9c04f4e6 --- /dev/null +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Avalonia.OpenGL +{ + public class AngleOptions + { + public enum PlatformApi + { + DirectX9, + DirectX11 + } + + public List AllowedPlatformApis = new List + { + PlatformApi.DirectX9 + }; + } +} diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index b14932acfe..b2b5a1a646 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using Avalonia.Platform.Interop; using static Avalonia.OpenGL.EglConsts; @@ -13,21 +14,42 @@ namespace Avalonia.OpenGL private readonly int[] _contextAttributes; public IntPtr Handle => _display; + private AngleOptions.PlatformApi? _angleApi; public EglDisplay(EglInterface egl) { _egl = egl; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _egl.GetPlatformDisplayEXT != null) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - foreach (var dapi in new[] {EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE}) + if (_egl.GetPlatformDisplayEXT == null) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); + + var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis + ?? new List {AngleOptions.PlatformApi.DirectX9}; + + foreach (var platformApi in allowedApis) { + int dapi; + if (platformApi == AngleOptions.PlatformApi.DirectX9) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; + else if (platformApi == AngleOptions.PlatformApi.DirectX11) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; + else + continue; + _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE }); - if(_display != IntPtr.Zero) + if (_display != IntPtr.Zero) + { + _angleApi = platformApi; break; + } } + + if (_display == IntPtr.Zero) + throw new OpenGlException("Unable to create ANGLE display"); } if (_display == IntPtr.Zero) @@ -64,29 +86,35 @@ namespace Avalonia.OpenGL if (!_egl.BindApi(cfg.Api)) continue; - var attribs = new[] + foreach(var stencilSize in new[]{8, 1, 0}) + foreach (var depthSize in new []{8, 1, 0}) { - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, - EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, 8, - EGL_STENCIL_SIZE, 8, - EGL_DEPTH_SIZE, 8, - EGL_NONE - }; - if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs)) - continue; - if (numConfigs == 0) - continue; - _contextAttributes = cfg.Attributes; - Type = cfg.Type; + var attribs = new[] + { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + + EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, + + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_STENCIL_SIZE, stencilSize, + EGL_DEPTH_SIZE, depthSize, + EGL_NONE + }; + if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs)) + continue; + if (numConfigs == 0) + continue; + _contextAttributes = cfg.Attributes; + Type = cfg.Type; + } } if (_contextAttributes == null) throw new OpenGlException("No suitable EGL config was found"); - + GlInterface = GlInterface.FromNativeUtf8GetProcAddress(b => _egl.GetProcAddress(b)); } @@ -97,6 +125,7 @@ namespace Avalonia.OpenGL public GlDisplayType Type { get; } public GlInterface GlInterface { get; } + public EglInterface EglInterface => _egl; public IGlContext CreateContext(IGlContext share) { var shareCtx = (EglContext)share; diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs index f5dd413b0f..d2e4543af3 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs @@ -26,31 +26,44 @@ namespace Avalonia.OpenGL public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() { var glSurface = _display.CreateWindowSurface(_info.Handle); - return new RenderTarget(_context, glSurface, _info); + return new RenderTarget(_display, _context, glSurface, _info); } - class RenderTarget : IGlPlatformSurfaceRenderTarget + class RenderTarget : IGlPlatformSurfaceRenderTargetWithCorruptionInfo { + private readonly EglDisplay _display; private readonly EglContext _context; private readonly EglSurface _glSurface; private readonly IEglWindowGlPlatformSurfaceInfo _info; + private PixelSize _initialSize; - public RenderTarget(EglContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) + public RenderTarget(EglDisplay display, EglContext context, + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) { + _display = display; _context = context; _glSurface = glSurface; _info = info; + _initialSize = info.Size; } public void Dispose() => _glSurface.Dispose(); + public bool IsCorrupted => _initialSize != _info.Size; + public IGlPlatformSurfaceRenderingSession BeginDraw() { var l = _context.Lock(); try { + if (IsCorrupted) + throw new RenderTargetCorruptedException(); _context.MakeCurrent(_glSurface); - return new Session(_context, _glSurface, _info, l); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(); + + return new Session(_display, _context, _glSurface, _info, l); } catch { @@ -61,15 +74,19 @@ namespace Avalonia.OpenGL class Session : IGlPlatformSurfaceRenderingSession { - private readonly IGlContext _context; + private readonly EglContext _context; private readonly EglSurface _glSurface; private readonly IEglWindowGlPlatformSurfaceInfo _info; + private readonly EglDisplay _display; private IDisposable _lock; + - public Session(IGlContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info, + public Session(EglDisplay display, EglContext context, + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info, IDisposable @lock) { _context = context; + _display = display; _glSurface = glSurface; _info = info; _lock = @lock; @@ -78,7 +95,11 @@ namespace Avalonia.OpenGL public void Dispose() { _context.Display.GlInterface.Flush(); + _display.EglInterface.WaitGL(); _glSurface.SwapBuffers(); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(); _context.Display.ClearContext(); _lock.Dispose(); } diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index 00fcd97af0..0a99778ddf 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Avalonia.Platform; using Avalonia.Platform.Interop; @@ -15,13 +16,28 @@ namespace Avalonia.OpenGL { } + [DllImport("libegl.dll", CharSet = CharSet.Ansi)] + static extern IntPtr eglGetProcAddress(string proc); + static Func Load() { var os = AvaloniaLocator.Current.GetService().GetRuntimeInfo().OperatingSystem; if(os == OperatingSystemType.Linux || os == OperatingSystemType.Android) return Load("libEGL.so.1"); if (os == OperatingSystemType.WinNT) - return Load(@"libegl.dll"); + { + var disp = eglGetProcAddress("eglGetPlatformDisplayEXT"); + if (disp == IntPtr.Zero) + throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point"); + return (name, optional) => + { + var r = eglGetProcAddress(name); + if (r == IntPtr.Zero && !optional) + throw new OpenGlException($"Entry point {r} is not found"); + return r; + }; + } + throw new PlatformNotSupportedException(); } @@ -91,6 +107,31 @@ namespace Avalonia.OpenGL [GlEntryPoint("eglGetConfigAttrib")] public EglGetConfigAttrib GetConfigAttrib { get; } + public delegate bool EglWaitGL(); + [GlEntryPoint("eglWaitGL")] + public EglWaitGL WaitGL { get; } + + public delegate bool EglWaitClient(); + [GlEntryPoint("eglWaitClient")] + public EglWaitGL WaitClient { get; } + + public delegate bool EglWaitNative(); + [GlEntryPoint("eglWaitNative")] + public EglWaitGL WaitNative { get; } + + public delegate IntPtr EglQueryString(IntPtr display, int i); + + [GlEntryPoint("eglQueryString")] + public EglQueryString QueryStringNative { get; } + + public string QueryString(IntPtr display, int i) + { + var rv = QueryStringNative(display, i); + if (rv == IntPtr.Zero) + return null; + return Marshal.PtrToStringAnsi(rv); + } + // ReSharper restore UnassignedGetOnlyAutoProperty } } diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs index 53da93315c..d198d46e5c 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs +++ b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs @@ -6,4 +6,9 @@ namespace Avalonia.OpenGL { IGlPlatformSurfaceRenderingSession BeginDraw(); } -} \ No newline at end of file + + public interface IGlPlatformSurfaceRenderTargetWithCorruptionInfo : IGlPlatformSurfaceRenderTarget + { + bool IsCorrupted { get; } + } +} diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index f67cb7f40a..ced26a3004 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -21,9 +21,8 @@ namespace Avalonia.ReactiveUI return builder.AfterSetup(_ => { RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; - Locator.CurrentMutable.Register( - () => new AvaloniaActivationForViewFetcher(), - typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); }); } } diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs new file mode 100644 index 0000000000..3f41f54363 --- /dev/null +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Layout; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Templates; +using ReactiveUI; + +namespace Avalonia.ReactiveUI +{ + /// + /// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls + /// that don't have DataTemplates, and assigns a default DataTemplate that + /// loads the View associated with each ViewModel. + /// + public class AutoDataTemplateBindingHook : IPropertyBindingHook + { + private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate(x => + { + var control = new ViewModelViewHost(); + var context = control.GetObservable(Control.DataContextProperty); + control.Bind(ViewModelViewHost.ViewModelProperty, context); + control.HorizontalContentAlignment = HorizontalAlignment.Stretch; + control.VerticalContentAlignment = VerticalAlignment.Stretch; + return control; + }, + true); + + /// + public bool ExecuteHook( + object source, object target, + Func[]> getCurrentViewModelProperties, + Func[]> getCurrentViewProperties, + BindingDirection direction) + { + var viewProperties = getCurrentViewProperties(); + var lastViewProperty = viewProperties.LastOrDefault(); + var itemsControl = lastViewProperty?.Sender as ItemsControl; + if (itemsControl == null) + return true; + + var propertyName = viewProperties.Last().GetPropertyName(); + if (propertyName != "Items" && + propertyName != "ItemsSource") + return true; + + if (itemsControl.ItemTemplate != null) + return true; + + itemsControl.ItemTemplate = DefaultItemTemplate; + return true; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj index 1851f9fb70..64145b9c3c 100644 --- a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj +++ b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj @@ -1,6 +1,7 @@  netstandard2.0 + Avalonia.ReactiveUI diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 4bd86a67c0..05edeea683 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -53,33 +53,13 @@ namespace Avalonia.ReactiveUI /// ReactiveUI routing documentation website for more info. /// /// - public class RoutedViewHost : UserControl, IActivatable, IEnableLogger + public class RoutedViewHost : TransitioningContentControl, IActivatable, IEnableLogger { /// - /// The router dependency property. + /// for the property. /// public static readonly AvaloniaProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); - - /// - /// The default content property. - /// - public static readonly AvaloniaProperty DefaultContentProperty = - AvaloniaProperty.Register(nameof(DefaultContent)); - - /// - /// Fade in animation property. - /// - public static readonly AvaloniaProperty FadeInAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); - - /// - /// Fade out animation property. - /// - public static readonly AvaloniaProperty FadeOutAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); /// /// Initializes a new instance of the class. @@ -104,42 +84,6 @@ namespace Avalonia.ReactiveUI set => SetValue(RouterProperty, value); } - /// - /// Gets or sets the content displayed whenever there is no page currently routed. - /// - public object DefaultContent - { - get => GetValue(DefaultContentProperty); - set => SetValue(DefaultContentProperty, value); - } - - /// - /// Gets or sets the animation played when page appears. - /// - public IAnimation FadeInAnimation - { - get => GetValue(FadeInAnimationProperty); - set => SetValue(FadeInAnimationProperty, value); - } - - /// - /// Gets or sets the animation played when page disappears. - /// - public IAnimation FadeOutAnimation - { - get => GetValue(FadeOutAnimationProperty); - set => SetValue(FadeOutAnimationProperty, value); - } - - /// - /// Duplicates the Content property with a private setter. - /// - public new object Content - { - get => base.Content; - private set => base.Content = value; - } - /// /// Gets or sets the ReactiveUI view locator used by this router. /// @@ -149,82 +93,29 @@ namespace Avalonia.ReactiveUI /// Invoked when ReactiveUI router navigates to a view model. /// /// ViewModel to which the user navigates. - /// - /// Thrown when ViewLocator is unable to find the appropriate view. - /// - private void NavigateToViewModel(IRoutableViewModel viewModel) + private void NavigateToViewModel(object viewModel) { if (viewModel == null) { - this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(DefaultContent); + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; return; } var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var view = viewLocator.ResolveView(viewModel); - if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } - this.Log().Info($"Ready to show {view} with autowired {viewModel}."); - view.ViewModel = viewModel; - if (view is IStyledElement styled) + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) styled.DataContext = viewModel; - UpdateContent(view); - } - - /// - /// Updates the content with transitions. - /// - /// New content to set. - private async void UpdateContent(object newContent) - { - if (FadeOutAnimation != null) - await FadeOutAnimation.RunAsync(this, Clock); - Content = newContent; - if (FadeInAnimation != null) - await FadeInAnimation.RunAsync(this, Clock); - } - - /// - /// Creates opacity animation for this routed view host. - /// - /// Opacity to start from. - /// Opacity to finish with. - /// Duration of the animation. - /// Animation object instance. - private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) - { - return new Avalonia.Animation.Animation - { - Duration = duration, - Children = - { - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = from - } - }, - Cue = new Cue(0d) - }, - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = to - } - }, - Cue = new Cue(1d) - } - } - }; + Content = viewInstance; } } -} +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs new file mode 100644 index 0000000000..1bec5fc365 --- /dev/null +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -0,0 +1,75 @@ +// 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.Animation; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.ReactiveUI +{ + /// + /// A ContentControl that animates the transition when its content is changed. + /// + public class TransitioningContentControl : ContentControl, IStyleable + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty PageTransitionProperty = + AvaloniaProperty.Register(nameof(PageTransition), + new CrossFade(TimeSpan.FromSeconds(0.5))); + + /// + /// for the property. + /// + public static readonly AvaloniaProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + /// + /// Gets or sets the animation played when content appears and disappears. + /// + public IPageTransition PageTransition + { + get => GetValue(PageTransitionProperty); + set => SetValue(PageTransitionProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Gets or sets the content with animation. + /// + public new object Content + { + get => base.Content; + set => UpdateContentWithTransition(value); + } + + /// + /// TransitioningContentControl uses the default ContentControl + /// template from Avalonia default theme. + /// + Type IStyleable.StyleKey => typeof(ContentControl); + + /// + /// Updates the content with transitions. + /// + /// New content to set. + private async void UpdateContentWithTransition(object content) + { + if (PageTransition != null) + await PageTransition.Start(this, null, true); + base.Content = content; + if (PageTransition != null) + await PageTransition.Start(null, this, true); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs new file mode 100644 index 0000000000..5cfa464c37 --- /dev/null +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -0,0 +1,80 @@ +// 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 System.Reactive.Disposables; +using ReactiveUI; +using Splat; + +namespace Avalonia.ReactiveUI +{ + /// + /// This content control will automatically load the View associated with + /// the ViewModel property and display it. This control is very useful + /// inside a DataTemplate to display the View associated with a ViewModel. + /// + public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty ViewModelProperty = + AvaloniaProperty.Register(nameof(ViewModel)); + + /// + /// Initializes a new instance of the class. + /// + public ViewModelViewHost() + { + this.WhenActivated(disposables => + { + this.WhenAnyValue(x => x.ViewModel) + .Subscribe(NavigateToViewModel) + .DisposeWith(disposables); + }); + } + + /// + /// Gets or sets the ViewModel to display. + /// + public object ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + /// Gets or sets the view locator. + /// + public IViewLocator ViewLocator { get; set; } + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + private void NavigateToViewModel(object viewModel) + { + if (viewModel == null) + { + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; + return; + } + + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } + + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) + styled.DataContext = viewModel; + Content = viewInstance; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Styling/Controls/IResourceProvider.cs b/src/Avalonia.Styling/Controls/IResourceProvider.cs index eec783623c..cbaacee012 100644 --- a/src/Avalonia.Styling/Controls/IResourceProvider.cs +++ b/src/Avalonia.Styling/Controls/IResourceProvider.cs @@ -28,6 +28,6 @@ namespace Avalonia.Controls /// /// True if the resource if found, otherwise false. /// - bool TryGetResource(string key, out object value); + bool TryGetResource(object key, out object value); } } diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Styling/Controls/ResourceDictionary.cs index 74a861b36b..901e27b7b7 100644 --- a/src/Avalonia.Styling/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/ResourceDictionary.cs @@ -69,7 +69,7 @@ namespace Avalonia.Controls } /// - public bool TryGetResource(string key, out object value) + public bool TryGetResource(object key, out object value) { if (TryGetValue(key, out value)) { diff --git a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs index 52309b87a2..01112eaf2c 100644 --- a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs +++ b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls /// The control. /// The resource key. /// The resource, or if not found. - public static object FindResource(this IResourceNode control, string key) + public static object FindResource(this IResourceNode control, object key) { if (control.TryFindResource(key, out var value)) { @@ -28,7 +28,7 @@ namespace Avalonia.Controls /// The resource key. /// On return, contains the resource if found, otherwise null. /// True if the resource was found; otherwise false. - public static bool TryFindResource(this IResourceNode control, string key, out object value) + public static bool TryFindResource(this IResourceNode control, object key, out object value) { Contract.Requires(control != null); Contract.Requires(key != null); @@ -52,7 +52,7 @@ namespace Avalonia.Controls return false; } - public static IObservable GetResourceObservable(this IResourceNode target, string key) + public static IObservable GetResourceObservable(this IResourceNode target, object key) { return new ResourceObservable(target, key); } @@ -60,9 +60,9 @@ namespace Avalonia.Controls private class ResourceObservable : LightweightObservableBase { private readonly IResourceNode _target; - private readonly string _key; + private readonly object _key; - public ResourceObservable(IResourceNode target, string key) + public ResourceObservable(IResourceNode target, object key) { _target = target; _key = key; diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index d314a8d44e..f7e063dfb5 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -415,7 +415,7 @@ namespace Avalonia } /// - bool IResourceProvider.TryGetResource(string key, out object value) + bool IResourceProvider.TryGetResource(object key, out object value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || @@ -677,23 +677,6 @@ namespace Avalonia if (Name != null) { _nameScope?.Register(Name, this); - - var visualParent = Parent as StyledElement; - - if (this is INameScope && visualParent != null) - { - // If we have e.g. a named UserControl in a window then we want that control - // to be findable by name from the Window, so register with both name scopes. - // This differs from WPF's behavior in that XAML manually registers controls - // with name scopes based on the XAML file in which the name attribute appears, - // but we're trying to avoid XAML magic in Avalonia in order to made code- - // created UIs easy. This will cause problems if a UserControl declares a name - // in its XAML and that control is included multiple times in a parent control - // (as the name will be duplicated), however at the moment I'm fine with saying - // "don't do that". - var parentNameScope = NameScope.FindNameScope(visualParent); - parentNameScope?.Register(Name, this); - } } } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index d799df7ac9..3ce82b4160 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -171,7 +171,7 @@ namespace Avalonia.Styling } /// - public bool TryGetResource(string key, out object result) + public bool TryGetResource(object key, out object result) { result = null; return _resources?.TryGetResource(key, out result) ?? false; diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 288cf35d08..789bb6ffd3 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -178,7 +178,7 @@ namespace Avalonia.Styling } /// - public bool TryGetResource(string key, out object value) + public bool TryGetResource(object key, out object value) { if (_resources != null && _resources.TryGetValue(key, out value)) { diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index f84e09510b..0ed17fae76 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -22,6 +22,7 @@ #FF808080 #FF119EDA + #FFFFFFFF #FFFF0000 #10FF0000 @@ -39,6 +40,7 @@ + @@ -46,11 +48,11 @@ - - - - - + + + + + 1,1,1,1 0.5 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 18c32b02bc..3a8a8ec446 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -22,6 +22,7 @@ #FF808080 #FF086F9E + #FFFFFFFF #FFFF0000 #10FF0000 @@ -39,6 +40,7 @@ + @@ -46,11 +48,11 @@ - - - - - + + + + + 1 0.5 diff --git a/src/Avalonia.Themes.Default/Button.xaml b/src/Avalonia.Themes.Default/Button.xaml index 698ddec2a8..6ed1f6d8fc 100644 --- a/src/Avalonia.Themes.Default/Button.xaml +++ b/src/Avalonia.Themes.Default/Button.xaml @@ -22,13 +22,13 @@ - - - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/NotificationCard.xaml b/src/Avalonia.Themes.Default/NotificationCard.xaml index e94cb33d1e..47d5988e8c 100644 --- a/src/Avalonia.Themes.Default/NotificationCard.xaml +++ b/src/Avalonia.Themes.Default/NotificationCard.xaml @@ -13,7 +13,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Margin="8,8,0,0"> - + @@ -40,6 +40,10 @@ + + \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml index 6741bdc7d9..ec1f6e9e7d 100644 --- a/src/Avalonia.Themes.Default/TextBox.xaml +++ b/src/Avalonia.Themes.Default/TextBox.xaml @@ -3,6 +3,8 @@ + + @@ -44,7 +46,10 @@ SelectionEnd="{TemplateBinding SelectionEnd}" TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="{TemplateBinding TextWrapping}" - PasswordChar="{TemplateBinding PasswordChar}"/> + PasswordChar="{TemplateBinding PasswordChar}" + SelectionBrush="{TemplateBinding SelectionBrush}" + SelectionForegroundBrush="{TemplateBinding SelectionForegroundBrush}" + CaretBrush="{TemplateBinding CaretBrush}"/> diff --git a/src/Avalonia.Themes.Default/ToggleButton.xaml b/src/Avalonia.Themes.Default/ToggleButton.xaml index 9e05c38eef..41f366fdf9 100644 --- a/src/Avalonia.Themes.Default/ToggleButton.xaml +++ b/src/Avalonia.Themes.Default/ToggleButton.xaml @@ -22,17 +22,17 @@ - - - - \ No newline at end of file + diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 6b8cb8b755..614d828259 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -14,8 +14,8 @@ namespace Avalonia.Animation /// public class CrossFade : IPageTransition { - private Animation _fadeOutAnimation; - private Animation _fadeInAnimation; + private readonly Animation _fadeOutAnimation; + private readonly Animation _fadeInAnimation; /// /// Initializes a new instance of the class. @@ -61,10 +61,10 @@ namespace Avalonia.Animation new Setter { Property = Visual.OpacityProperty, - Value = 0d + Value = 1d } }, - Cue = new Cue(0d) + Cue = new Cue(1d) } } diff --git a/src/Avalonia.Visuals/Platform/IRenderTarget.cs b/src/Avalonia.Visuals/Platform/IRenderTarget.cs index 522de64ec7..516bea782e 100644 --- a/src/Avalonia.Visuals/Platform/IRenderTarget.cs +++ b/src/Avalonia.Visuals/Platform/IRenderTarget.cs @@ -23,4 +23,9 @@ namespace Avalonia.Platform /// IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer); } + + public interface IRenderTargetWithCorruptionInfo : IRenderTarget + { + bool IsCorrupted { get; } + } } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 5d1c66f872..0d077d2a3a 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -245,6 +245,11 @@ namespace Avalonia.Rendering { if (context != null) return context; + if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + { + RenderTarget.Dispose(); + RenderTarget = null; + } if (RenderTarget == null) RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); return context = RenderTarget.CreateDrawingContext(this); @@ -528,6 +533,8 @@ namespace Avalonia.Rendering oldScene?.Dispose(); } + _dirty.Clear(); + if (SceneInvalidated != null) { var rect = new Rect(); @@ -540,10 +547,9 @@ namespace Avalonia.Rendering } } + System.Diagnostics.Debug.WriteLine("Invalidated " + rect); SceneInvalidated(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect)); } - - _dirty.Clear(); } else { diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs index 75d8f036d6..2d4a39e026 100644 --- a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs +++ b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs @@ -7,11 +7,25 @@ namespace Avalonia.Rendering public class ManagedDeferredRendererLock : IDeferredRendererLock { private readonly object _lock = new object(); + + /// + /// Tries to lock the target surface or window + /// + /// IDisposable if succeeded to obtain the lock public IDisposable TryLock() { if (Monitor.TryEnter(_lock)) return Disposable.Create(() => Monitor.Exit(_lock)); return null; } + + /// + /// Enters a waiting lock, only use from platform code, not from the renderer + /// + public IDisposable Lock() + { + Monitor.Enter(_lock); + return Disposable.Create(() => Monitor.Exit(_lock)); + } } } diff --git a/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs b/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs new file mode 100644 index 0000000000..dd6cf7ad15 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs @@ -0,0 +1,32 @@ +using System; +using System.Diagnostics; +using System.Reactive.Disposables; +using Avalonia.Threading; + +namespace Avalonia.Rendering +{ + /// + /// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms + /// + + public class UiThreadRenderTimer : DefaultRenderTimer + { + public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond) + { + } + + protected override IDisposable StartCore(Action tick) + { + bool cancelled = false; + var st = Stopwatch.StartNew(); + DispatcherTimer.Run(() => + { + if (cancelled) + return false; + tick(st.Elapsed); + return !cancelled; + }, TimeSpan.FromSeconds(1.0 / FramesPerSecond), DispatcherPriority.Render); + return Disposable.Create(() => cancelled = true); + } + } +} diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index 1629890568..59afc877de 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -1,8 +1,8 @@  - netstandard2.0 true + Avalonia.X11 diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 40b01117e3..0a8b1ee9c4 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -8,6 +8,8 @@ namespace Avalonia.X11 { class X11CursorFactory : IStandardCursorFactory { + private static IntPtr _nullCursor; + private readonly IntPtr _display; private Dictionary _cursors; @@ -42,16 +44,34 @@ namespace Avalonia.X11 public X11CursorFactory(IntPtr display) { _display = display; + _nullCursor = GetNullCursor(display); _cursors = Enum.GetValues(typeof(CursorFontShape)).Cast() .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } - + public IPlatformHandle GetCursor(StandardCursorType cursorType) { - var handle = s_mapping.TryGetValue(cursorType, out var shape) + IntPtr handle; + if (cursorType == StandardCursorType.None) + { + handle = _nullCursor; + } + else + { + handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; + } return new PlatformHandle(handle, "XCURSOR"); } + + private static IntPtr GetNullCursor(IntPtr display) + { + XColor color = new XColor(); + byte[] data = new byte[] { 0 }; + IntPtr window = XLib.XRootWindow(display, 0); + IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, data, 1, 1); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + } } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ce03113169..cf5902eff7 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -28,6 +28,7 @@ namespace Avalonia.X11 public X11PlatformOptions Options { get; private set; } public void Initialize(X11PlatformOptions options) { + Options = options; XInitThreads(); Display = XOpenDisplay(IntPtr.Zero); DeferredDisplay = XOpenDisplay(IntPtr.Zero); @@ -66,7 +67,7 @@ namespace Avalonia.X11 GlxGlPlatformFeature.TryInitialize(Info); } - Options = options; + } public IntPtr DeferredDisplay { get; set; } @@ -96,6 +97,7 @@ namespace Avalonia public bool UseEGL { get; set; } public bool UseGpu { get; set; } = true; public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; + public bool? EnableMultiTouch { get; set; } } public static class AvaloniaX11PlatformExtensions { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index c5e77fe352..01beebfff1 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -314,9 +314,9 @@ namespace Avalonia.X11 else if (ev.type == XEventName.FocusOut) Deactivated?.Invoke(); else if (ev.type == XEventName.MotionNotify) - MouseEvent(RawMouseEventType.Move, ref ev, ev.MotionEvent.state); + MouseEvent(RawPointerEventType.Move, ref ev, ev.MotionEvent.state); else if (ev.type == XEventName.LeaveNotify) - MouseEvent(RawMouseEventType.LeaveWindow, ref ev, ev.CrossingEvent.state); + MouseEvent(RawPointerEventType.LeaveWindow, ref ev, ev.CrossingEvent.state); else if (ev.type == XEventName.PropertyNotify) { OnPropertyChange(ev.PropertyEvent.atom, ev.PropertyEvent.state == 0); @@ -326,9 +326,9 @@ namespace Avalonia.X11 if (ActivateTransientChildIfNeeded()) return; if (ev.ButtonEvent.button < 4) - MouseEvent(ev.ButtonEvent.button == 1 ? RawMouseEventType.LeftButtonDown - : ev.ButtonEvent.button == 2 ? RawMouseEventType.MiddleButtonDown - : RawMouseEventType.RightButtonDown, ref ev, ev.ButtonEvent.state); + MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonDown + : ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonDown + : RawPointerEventType.RightButtonDown, ref ev, ev.ButtonEvent.state); else { var delta = ev.ButtonEvent.button == 4 @@ -347,9 +347,9 @@ namespace Avalonia.X11 else if (ev.type == XEventName.ButtonRelease) { if (ev.ButtonEvent.button < 4) - MouseEvent(ev.ButtonEvent.button == 1 ? RawMouseEventType.LeftButtonUp - : ev.ButtonEvent.button == 2 ? RawMouseEventType.MiddleButtonUp - : RawMouseEventType.RightButtonUp, ref ev, ev.ButtonEvent.state); + MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonUp + : ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonUp + : RawPointerEventType.RightButtonUp, ref ev, ev.ButtonEvent.state); } else if (ev.type == XEventName.ConfigureNotify) { @@ -577,7 +577,7 @@ namespace Avalonia.X11 public void ScheduleInput(RawInputEventArgs args) { - if (args is RawMouseEventArgs mouse) + if (args is RawPointerEventArgs mouse) mouse.Position = mouse.Position / Scaling; if (args is RawDragEvent drag) drag.Location = drag.Location / Scaling; @@ -598,13 +598,13 @@ namespace Avalonia.X11 } } - void MouseEvent(RawMouseEventType type, ref XEvent ev, XModifierMask mods) + void MouseEvent(RawPointerEventType type, ref XEvent ev, XModifierMask mods) { - var mev = new RawMouseEventArgs( + var mev = new RawPointerEventArgs( _mouse, (ulong)ev.ButtonEvent.time.ToInt64(), _inputRoot, type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods)); - if(type == RawMouseEventType.Move && _inputQueue.Count>0 && _lastEvent.Event is RawMouseEventArgs ma) - if (ma.Type == RawMouseEventType.Move) + if(type == RawPointerEventType.Move && _inputQueue.Count>0 && _lastEvent.Event is RawPointerEventArgs ma) + if (ma.Type == RawPointerEventType.Move) { _lastEvent.Event = mev; return; diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index ee73ccc907..0a78c0dfd9 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -11,6 +11,7 @@ namespace Avalonia.X11 unsafe class XI2Manager { private X11Info _x11; + private bool _multitouch; private Dictionary _clients = new Dictionary(); class DeviceInfo { @@ -77,11 +78,14 @@ namespace Avalonia.X11 private PointerDeviceInfo _pointerDevice; private AvaloniaX11Platform _platform; + private readonly TouchDevice _touchDevice = new TouchDevice(); + public bool Init(AvaloniaX11Platform platform) { _platform = platform; _x11 = platform.Info; + _multitouch = platform.Options?.EnableMultiTouch ?? false; var devices =(XIDeviceInfo*) XIQueryDevice(_x11.Display, (int)XiPredefinedDeviceId.XIAllMasterDevices, out int num); for (var c = 0; c < num; c++) @@ -121,16 +125,23 @@ namespace Avalonia.X11 public XEventMask AddWindow(IntPtr xid, IXI2Client window) { _clients[xid] = window; - - XiSelectEvents(_x11.Display, xid, new Dictionary> + var events = new List { - [_pointerDevice.Id] = new List() + XiEventType.XI_Motion, + XiEventType.XI_ButtonPress, + XiEventType.XI_ButtonRelease + }; + + if (_multitouch) + events.AddRange(new[] { - XiEventType.XI_Motion, - XiEventType.XI_ButtonPress, - XiEventType.XI_ButtonRelease, - } - }); + XiEventType.XI_TouchBegin, + XiEventType.XI_TouchUpdate, + XiEventType.XI_TouchEnd + }); + + XiSelectEvents(_x11.Display, xid, + new Dictionary> {[_pointerDevice.Id] = events}); // We are taking over mouse input handling from here return XEventMask.PointerMotionMask @@ -154,8 +165,9 @@ namespace Avalonia.X11 _pointerDevice.Update(changed->Classes, changed->NumClasses); } - //TODO: this should only be used for non-touch devices - if (xev->evtype >= XiEventType.XI_ButtonPress && xev->evtype <= XiEventType.XI_Motion) + + if ((xev->evtype >= XiEventType.XI_ButtonPress && xev->evtype <= XiEventType.XI_Motion) + || (xev->evtype>=XiEventType.XI_TouchBegin&&xev->evtype<=XiEventType.XI_TouchEnd)) { var dev = (XIDeviceEvent*)xev; if (_clients.TryGetValue(dev->EventWindow, out var client)) @@ -165,6 +177,23 @@ namespace Avalonia.X11 void OnDeviceEvent(IXI2Client client, ParsedDeviceEvent ev) { + if (ev.Type == XiEventType.XI_TouchBegin + || ev.Type == XiEventType.XI_TouchUpdate + || ev.Type == XiEventType.XI_TouchEnd) + { + var type = ev.Type == XiEventType.XI_TouchBegin ? + RawPointerEventType.TouchBegin : + (ev.Type == XiEventType.XI_TouchUpdate ? + RawPointerEventType.TouchUpdate : + RawPointerEventType.TouchEnd); + client.ScheduleInput(new RawTouchEventArgs(_touchDevice, + ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail)); + return; + } + + if (_multitouch && ev.Emulated) + return; + if (ev.Type == XiEventType.XI_Motion) { Vector scrollDelta = default; @@ -194,23 +223,23 @@ namespace Avalonia.X11 client.ScheduleInput(new RawMouseWheelEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, ev.Position, scrollDelta, ev.Modifiers)); if (_pointerDevice.HasMotion(ev)) - client.ScheduleInput(new RawMouseEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, - RawMouseEventType.Move, ev.Position, ev.Modifiers)); + client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, + RawPointerEventType.Move, ev.Position, ev.Modifiers)); } if (ev.Type == XiEventType.XI_ButtonPress || ev.Type == XiEventType.XI_ButtonRelease) { var down = ev.Type == XiEventType.XI_ButtonPress; var type = - ev.Button == 1 ? (down ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp) - : ev.Button == 2 ? (down ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp) - : ev.Button == 3 ? (down ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp) - : (RawMouseEventType?)null; + ev.Button == 1 ? (down ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp) + : ev.Button == 2 ? (down ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp) + : ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp) + : (RawPointerEventType?)null; if (type.HasValue) - client.ScheduleInput(new RawMouseEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, + client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, type.Value, ev.Position, ev.Modifiers)); } - + _pointerDevice.UpdateValuators(ev.Valuators); } } @@ -222,6 +251,8 @@ namespace Avalonia.X11 public ulong Timestamp { get; } public Point Position { get; } public int Button { get; set; } + public int Detail { get; set; } + public bool Emulated { get; set; } public Dictionary Valuators { get; } public ParsedDeviceEvent(XIDeviceEvent* ev) { @@ -258,6 +289,8 @@ namespace Avalonia.X11 Valuators[c] = *values++; if (Type == XiEventType.XI_ButtonPress || Type == XiEventType.XI_ButtonRelease) Button = ev->detail; + Detail = ev->detail; + Emulated = ev->flags.HasFlag(XiDeviceEventFlags.XIPointerEmulated); } } diff --git a/src/Avalonia.X11/XIStructs.cs b/src/Avalonia.X11/XIStructs.cs index ef49a72c43..4675ef47f2 100644 --- a/src/Avalonia.X11/XIStructs.cs +++ b/src/Avalonia.X11/XIStructs.cs @@ -230,13 +230,20 @@ namespace Avalonia.X11 public double root_y; public double event_x; public double event_y; - public int flags; + public XiDeviceEventFlags flags; public XIButtonState buttons; public XIValuatorState valuators; public XIModifierState mods; public XIModifierState group; } + [Flags] + public enum XiDeviceEventFlags : int + { + None = 0, + XIPointerEmulated = (1 << 16) + } + [StructLayout(LayoutKind.Sequential)] unsafe struct XIEvent { diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 8a146f922d..3c41f7bdde 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -321,6 +321,9 @@ namespace Avalonia.X11 public static extern IntPtr XCreatePixmapCursor(IntPtr display, IntPtr source, IntPtr mask, ref XColor foreground_color, ref XColor background_color, int x_hot, int y_hot); + [DllImport(libX11)] + public static extern IntPtr XCreateBitmapFromData(IntPtr display, IntPtr drawable, byte[] data, int width, int height); + [DllImport(libX11)] public static extern IntPtr XCreatePixmapFromBitmapData(IntPtr display, IntPtr drawable, byte[] data, int width, int height, IntPtr fg, IntPtr bg, int depth); diff --git a/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj b/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj index 0885ccccfa..95443a364e 100644 --- a/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj +++ b/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj @@ -3,6 +3,7 @@ netstandard2.0 true $(DefineConstants);GTK3_PINVOKE + Avalonia.Gtk3 diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index a28b1cbb1a..95fa3ba9e3 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -12,6 +12,7 @@ namespace Avalonia.Gtk3 private static readonly Dictionary CursorTypeMapping = new Dictionary { + {StandardCursorType.None, CursorType.Blank}, {StandardCursorType.AppStarting, CursorType.Watch}, {StandardCursorType.Arrow, CursorType.LeftPtr}, {StandardCursorType.Cross, CursorType.Cross}, @@ -80,4 +81,4 @@ namespace Avalonia.Gtk3 return rv; } } -} \ No newline at end of file +} diff --git a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs index 4fad8208b3..aa0f8cde0d 100644 --- a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs +++ b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs @@ -2,6 +2,7 @@ { enum GdkCursorType { + Blank = -2, CursorIsPixmap = -1, XCursor = 0, Arrow = 2, diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index c4697462d2..bff50a979d 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -145,17 +145,17 @@ namespace Avalonia.Gtk3 private unsafe bool OnButton(IntPtr w, IntPtr ev, IntPtr userdata) { var evnt = (GdkEventButton*)ev; - var e = new RawMouseEventArgs( + var e = new RawPointerEventArgs( Gtk3Platform.Mouse, evnt->time, _inputRoot, evnt->type == GdkEventType.ButtonRelease ? evnt->button == 1 - ? RawMouseEventType.LeftButtonUp - : evnt->button == 3 ? RawMouseEventType.RightButtonUp : RawMouseEventType.MiddleButtonUp + ? RawPointerEventType.LeftButtonUp + : evnt->button == 3 ? RawPointerEventType.RightButtonUp : RawPointerEventType.MiddleButtonUp : evnt->button == 1 - ? RawMouseEventType.LeftButtonDown - : evnt->button == 3 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown, + ? RawPointerEventType.LeftButtonDown + : evnt->button == 3 ? RawPointerEventType.RightButtonDown : RawPointerEventType.MiddleButtonDown, new Point(evnt->x, evnt->y), GetModifierKeys(evnt->state)); OnInput(e); return true; @@ -179,11 +179,11 @@ namespace Avalonia.Gtk3 var evnt = (GdkEventMotion*)ev; var position = new Point(evnt->x, evnt->y); Native.GdkEventRequestMotions(ev); - var e = new RawMouseEventArgs( + var e = new RawPointerEventArgs( Gtk3Platform.Mouse, evnt->time, _inputRoot, - RawMouseEventType.Move, + RawPointerEventType.Move, position, GetModifierKeys(evnt->state)); OnInput(e); @@ -237,10 +237,10 @@ namespace Avalonia.Gtk3 { var evnt = (GdkEventCrossing*) pev; var position = new Point(evnt->x, evnt->y); - OnInput(new RawMouseEventArgs(Gtk3Platform.Mouse, + OnInput(new RawPointerEventArgs(Gtk3Platform.Mouse, evnt->time, _inputRoot, - RawMouseEventType.Move, + RawPointerEventType.Move, position, GetModifierKeys(evnt->state))); return true; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj index c622bbb8c5..48095a4c25 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj +++ b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj @@ -2,6 +2,7 @@ netstandard2.0 true + Avalonia.LinuxFramebuffer diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs b/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs index 4fa57dbf00..b982b98d38 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs @@ -76,9 +76,9 @@ namespace Avalonia.LinuxFramebuffer _y = Math.Min(_height, Math.Max(0, _y + ev.value)); else return; - Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice, + Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice, LinuxFramebufferPlatform.Timestamp, - LinuxFramebufferPlatform.TopLevel.InputRoot, RawMouseEventType.Move, new Point(_x, _y), + LinuxFramebufferPlatform.TopLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y), InputModifiers.None)); } if (ev.type ==(int) EvType.EV_ABS) @@ -89,24 +89,24 @@ namespace Avalonia.LinuxFramebuffer _y = TranslateAxis(device.AbsY.Value, ev.value, _height); else return; - Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice, + Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice, LinuxFramebufferPlatform.Timestamp, - LinuxFramebufferPlatform.TopLevel.InputRoot, RawMouseEventType.Move, new Point(_x, _y), + LinuxFramebufferPlatform.TopLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y), InputModifiers.None)); } if (ev.type == (short) EvType.EV_KEY) { - RawMouseEventType? type = null; + RawPointerEventType? type = null; if (ev.code == (ushort) EvKey.BTN_LEFT) - type = ev.value == 1 ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp; + type = ev.value == 1 ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp; if (ev.code == (ushort)EvKey.BTN_RIGHT) - type = ev.value == 1 ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp; + type = ev.value == 1 ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp; if (ev.code == (ushort) EvKey.BTN_MIDDLE) - type = ev.value == 1 ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp; + type = ev.value == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp; if (!type.HasValue) return; - Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice, + Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice, LinuxFramebufferPlatform.Timestamp, LinuxFramebufferPlatform.TopLevel.InputRoot, type.Value, new Point(_x, _y), default(InputModifiers))); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 8ce29e5b8e..6f3dabd568 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -11,38 +11,22 @@ - - - - + - - - - - - - - - - - - - @@ -52,7 +36,6 @@ - @@ -76,11 +59,11 @@ - + diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs deleted file mode 100644 index f967bdf0af..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.ComponentModel; -using System.Collections.Generic; -using System.Globalization; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Markup.Xaml.Converters; -using Avalonia.Media.Imaging; -using Avalonia.Styling; -using Avalonia.Controls.Templates; - -namespace Avalonia.Markup.Xaml -{ - using System.Reflection; - using Avalonia.Media; - - /// - /// Maintains a repository of s for XAML parsing on top of those - /// maintained by . - /// - /// - /// The default method of defining type converters using - /// isn't powerful enough for our purposes: - /// - /// - It doesn't handle non-constructed generic types (such as ) - /// - Type converters which require XAML features cannot be defined in non-XAML assemblies and - /// so can't be referenced using - /// - Many types have a static `Parse(string)` method which can be used implicitly; this class - /// detects such methods and auto-creates a type converter - /// - public static class AvaloniaTypeConverters - { - // When adding item to that list make sure to modify AvaloniaXamlIlLanguage - private static Dictionary _converters = new Dictionary() - { - { typeof(AvaloniaList<>), typeof(AvaloniaListConverter<>) }, - { typeof(AvaloniaProperty), typeof(AvaloniaPropertyTypeConverter) }, - { typeof(IBitmap), typeof(BitmapTypeConverter) }, - { typeof(IList), typeof(PointsListTypeConverter) }, - { typeof(IMemberSelector), typeof(MemberSelectorTypeConverter) }, - { typeof(Selector), typeof(SelectorTypeConverter) }, - { typeof(TimeSpan), typeof(TimeSpanTypeConverter) }, - { typeof(WindowIcon), typeof(IconTypeConverter) }, - { typeof(CultureInfo), typeof(CultureInfoConverter) }, - { typeof(Uri), typeof(AvaloniaUriTypeConverter) }, - { typeof(FontFamily), typeof(FontFamilyTypeConverter) }, - { typeof(EventInfo), typeof(AvaloniaEventConverter) }, - }; - - internal static Type GetBuiltinTypeConverter(Type type) - { - _converters.TryGetValue(type, out var result); - return result; - } - - /// - /// Tries to lookup a for a type. - /// - /// The type. - /// The type converter. - public static Type GetTypeConverter(Type type) - { - if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - var inner = GetTypeConverter(type.GetGenericArguments()[0]); - if (inner == null) - return null; - return typeof(NullableTypeConverter<>).MakeGenericType(inner); - } - - if (_converters.TryGetValue(type, out var result)) - { - return result; - } - - // Converters for non-constructed generic types can't be specified using - // TypeConverterAttribute. Allow them to be registered here and handle them sanely. - if (type.IsConstructedGenericType && - _converters.TryGetValue(type.GetGenericTypeDefinition(), out result)) - { - return result?.MakeGenericType(type.GetGenericArguments()); - } - - // If the type isn't a primitive or a type that XAML already handles, but has a static - // Parse method, use that - if (!type.IsPrimitive && - type != typeof(DateTime) && - type != typeof(Uri) && - ParseTypeConverter.HasParseMethod(type)) - { - result = typeof(ParseTypeConverter<>).MakeGenericType(type); - _converters.Add(type, result); - return result; - } - - _converters.Add(type, null); - return null; - } - - /// - /// Registers a type converter for a type. - /// - /// The type. Maybe be a non-constructed generic type. - /// The converter type. Maybe be a non-constructed generic type. - public static void Register(Type type, Type converterType) => _converters[type] = converterType; - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 45424e67bb..e8f2439f46 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -15,7 +15,6 @@ using Avalonia.Controls; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.PortableXaml; using Avalonia.Platform; -using Portable.Xaml; namespace Avalonia.Markup.Xaml { @@ -26,71 +25,14 @@ namespace Avalonia.Markup.Xaml { public bool IsDesignMode { get; set; } - public static bool UseLegacyXamlLoader { get; set; } = false; - - /// - /// Initializes a new instance of the class. - /// - public AvaloniaXamlLoader() - { - } - /// /// Loads the XAML into a Avalonia component. /// /// The object to load the XAML into. public static void Load(object obj) { - Contract.Requires(obj != null); - - var loader = new AvaloniaXamlLoader(); - loader.Load(obj.GetType(), obj); - } - - /// - /// Loads the XAML for a type. - /// - /// The type. - /// - /// The optional instance into which the XAML should be loaded. - /// - /// The loaded object. - public object Load(Type type, object rootInstance = null) - { - Contract.Requires(type != null); - - // HACK: Currently Visual Studio is forcing us to change the extension of xaml files - // in certain situations, so we try to load .xaml and if that's not found we try .xaml. - // Ideally we'd be able to use .xaml everywhere - var assetLocator = AvaloniaLocator.Current.GetService(); - - if (assetLocator == null) - { - throw new InvalidOperationException( - "Could not create IAssetLoader : maybe Application.RegisterServices() wasn't called?"); - } - - foreach (var uri in GetUrisFor(assetLocator, type)) - { - if (assetLocator.Exists(uri)) - { - using (var stream = assetLocator.Open(uri)) - { - var initialize = rootInstance as ISupportInitialize; - initialize?.BeginInit(); - try - { - return Load(stream, type.Assembly, rootInstance, uri); - } - finally - { - initialize?.EndInit(); - } - } - } - } - - throw new FileNotFoundException("Unable to find view for " + type.FullName); + throw new XamlLoadException( + $"No precompiled XAML found for {obj.GetType()}, make sure to specify x:Class and include your XAML file as AvaloniaResource"); } /// @@ -100,11 +42,8 @@ namespace Avalonia.Markup.Xaml /// /// A base URI to use if is relative. /// - /// - /// The optional instance into which the XAML should be loaded. - /// /// The loaded object. - public object Load(Uri uri, Uri baseUri = null, object rootInstance = null) + public object Load(Uri uri, Uri baseUri = null) { Contract.Requires(uri != null); @@ -133,7 +72,7 @@ namespace Avalonia.Markup.Xaml using (var stream = asset.stream) { var absoluteUri = uri.IsAbsoluteUri ? uri : new Uri(baseUri, uri); - return Load(stream, asset.assembly, rootInstance, absoluteUri); + return Load(stream, asset.assembly, null, absoluteUri); } } @@ -166,95 +105,9 @@ namespace Avalonia.Markup.Xaml /// /// The URI of the XAML /// The loaded object. - public object Load(Stream stream, Assembly localAssembly, object rootInstance = null, Uri uri = null) - { - if (!UseLegacyXamlLoader) - return AvaloniaXamlIlRuntimeCompiler.Load(stream, localAssembly, rootInstance, uri, IsDesignMode); - - - var readerSettings = new XamlXmlReaderSettings() - { - BaseUri = uri, - LocalAssembly = localAssembly, - ProvideLineInfo = true, - }; - - var context = IsDesignMode ? AvaloniaXamlSchemaContext.DesignInstance : AvaloniaXamlSchemaContext.Instance; - var reader = new XamlXmlReader(stream, context, readerSettings); - - object result = LoadFromReader( - reader, - AvaloniaXamlContext.For(readerSettings, rootInstance)); - - var topLevel = result as TopLevel; - - if (topLevel != null) - { - DelayedBinding.ApplyBindings(topLevel); - } - - return result; - } - - internal static object LoadFromReader(XamlReader reader, AvaloniaXamlContext context = null, IAmbientProvider parentAmbientProvider = null) - { - var writer = AvaloniaXamlObjectWriter.Create( - (AvaloniaXamlSchemaContext)reader.SchemaContext, - context, - parentAmbientProvider); + public object Load(Stream stream, Assembly localAssembly, object rootInstance = null, Uri uri = null) + => AvaloniaXamlIlRuntimeCompiler.Load(stream, localAssembly, rootInstance, uri, IsDesignMode); - XamlServices.Transform(reader, writer); - writer.ApplyAllDelayedProperties(); - return writer.Result; - } - - internal static object LoadFromReader(XamlReader reader) - { - //return XamlServices.Load(reader); - return LoadFromReader(reader, null); - } - - - private static readonly DataContractSerializer s_xamlInfoSerializer = - new DataContractSerializer(typeof(AvaloniaResourceXamlInfo)); - /// - /// Gets the URI for a type. - /// - /// - /// The type. - /// The URI. - private static IEnumerable GetUrisFor(IAssetLoader assetLocator, Type type) - { - var asm = type.GetTypeInfo().Assembly.GetName().Name; - var xamlInfoUri = new Uri($"avares://{asm}/!AvaloniaResourceXamlInfo"); - var typeName = type.FullName; - if (typeName == null) - throw new ArgumentException("Type doesn't have a FullName"); - - if (assetLocator.Exists(xamlInfoUri)) - { - using (var xamlInfoStream = assetLocator.Open(xamlInfoUri)) - { - var assetDoc = XDocument.Load(xamlInfoStream); - XNamespace assetNs = assetDoc.Root.Attribute("xmlns").Value; - XNamespace arrayNs = "http://schemas.microsoft.com/2003/10/Serialization/Arrays"; - Dictionary xamlInfo = - assetDoc.Root.Element(assetNs + "ClassToResourcePathIndex").Elements(arrayNs + "KeyValueOfstringstring") - .ToDictionary(entry =>entry.Element(arrayNs + "Key").Value, - entry => entry.Element(arrayNs + "Value").Value); - - if (xamlInfo.TryGetValue(typeName, out var rv)) - { - yield return new Uri($"avares://{asm}{rv}"); - yield break; - } - } - } - - yield return new Uri("resm:" + typeName + ".xaml?assembly=" + asm); - yield return new Uri("resm:" + typeName + ".paml?assembly=" + asm); - } - public static object Parse(string xaml, Assembly localAssembly = null) => new AvaloniaXamlLoader().Load(xaml, localAssembly); diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaEventConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaEventConverter.cs deleted file mode 100644 index 665e71bfea..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaEventConverter.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Avalonia.Controls; -using Avalonia.Markup.Xaml.PortableXaml; -using Portable.Xaml; - -namespace Avalonia.Markup.Xaml.Converters -{ - internal class AvaloniaEventConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - var text = value as string; - if (text != null) - { - var rootObjectProvider = context.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider; - var destinationTypeProvider = context.GetService(typeof(IDestinationTypeProvider)) as IDestinationTypeProvider; - if (rootObjectProvider != null && destinationTypeProvider != null) - { - var target = rootObjectProvider.RootObject; - var eventType = destinationTypeProvider.GetDestinationType(); - var eventParameters = eventType.GetRuntimeMethods().First(r => r.Name == "Invoke").GetParameters(); - // go in reverse to match System.Xaml behaviour - var methods = target.GetType().GetRuntimeMethods().Reverse(); - - // find based on exact match parameter types first - foreach (var method in methods) - { - if (method.Name != text) - continue; - var parameters = method.GetParameters(); - if (eventParameters.Length != parameters.Length) - continue; - if (parameters.Length == 0) - return method.CreateDelegate(eventType, target); - - for (int i = 0; i < parameters.Length; i++) - { - var param = parameters[i]; - var eventParam = eventParameters[i]; - if (param.ParameterType != eventParam.ParameterType) - break; - if (i == parameters.Length - 1) - return method.CreateDelegate(eventType, target); - } - } - - // EnhancedXaml: Find method with compatible base class parameters - foreach (var method in methods) - { - if (method.Name != text) - continue; - var parameters = method.GetParameters(); - if (parameters.Length == 0 || eventParameters.Length != parameters.Length) - continue; - - for (int i = 0; i < parameters.Length; i++) - { - var param = parameters[i]; - var eventParam = eventParameters[i]; - if (!param.ParameterType.GetTypeInfo().IsAssignableFrom(eventParam.ParameterType.GetTypeInfo())) - break; - if (i == parameters.Length - 1) - return method.CreateDelegate(eventType, target); - } - } - - var contextProvider = (IXamlSchemaContextProvider)context.GetService(typeof(IXamlSchemaContextProvider)); - var avaloniaContext = (AvaloniaXamlSchemaContext)contextProvider.SchemaContext; - - if (avaloniaContext.IsDesignMode) - { - // We want to ignore missing events in the designer, so if event handler - // wasn't found create an empty delegate. - var lambdaExpression = Expression.Lambda( - eventType, - Expression.Empty(), - eventParameters.Select(x => Expression.Parameter(x.ParameterType))); - return lambdaExpression.Compile(); - } - else - { - throw new XamlObjectWriterException($"Referenced value method {text} in type {target.GetType()} indicated by event {eventType.FullName} was not found"); - } - } - } - return base.ConvertFrom(context, culture, value); - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 18a7fe9ab6..b42bd53619 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -11,7 +11,6 @@ using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.Templates; using Avalonia.Styling; using Avalonia.Utilities; -using Portable.Xaml.ComponentModel; namespace Avalonia.Markup.Xaml.Converters { diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs index bfee7b953b..c75c54554e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs @@ -8,8 +8,7 @@ using Avalonia.Platform; namespace Avalonia.Markup.Xaml.Converters { - using Portable.Xaml.ComponentModel; - using System.ComponentModel; + using System.ComponentModel; public class BitmapTypeConverter : TypeConverter { diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs index 863e8fbbce..e92c155773 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs @@ -7,7 +7,6 @@ using System.Globalization; using Avalonia.Media; -using Portable.Xaml.ComponentModel; namespace Avalonia.Markup.Xaml.Converters { diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs index f3972ffe18..3a2f41bd3d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs @@ -9,7 +9,6 @@ using System.Globalization; namespace Avalonia.Markup.Xaml.Converters { - using Portable.Xaml.ComponentModel; using System.ComponentModel; public class IconTypeConverter : TypeConverter diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/NullableTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/NullableTypeConverter.cs deleted file mode 100644 index 5e7a31da56..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/NullableTypeConverter.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections; -using System.ComponentModel; -using System.Globalization; - -namespace Avalonia.Markup.Xaml.Converters -{ - public class NullableTypeConverter : TypeConverter where T : TypeConverter, new() - { - private TypeConverter _inner; - - public NullableTypeConverter() - { - _inner = new T(); - } - - public NullableTypeConverter(TypeConverter inner) - { - _inner = inner; - } - - - public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) - { - if (value == null) - return null; - return _inner.ConvertTo(context, culture, value, destinationType); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - if (value == null) - return null; - if (value as string == "") - return null; - return _inner.ConvertFrom(context, culture, value); - } - - public override object CreateInstance(ITypeDescriptorContext context, IDictionary propertyValues) - { - return _inner.CreateInstance(context, propertyValues); - } - - public override bool GetStandardValuesSupported(ITypeDescriptorContext context) - { - return _inner.GetStandardValuesSupported(context); - } - - public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) - { - return _inner.GetStandardValuesExclusive(context); - } - - public override bool GetCreateInstanceSupported(ITypeDescriptorContext context) - { - return _inner.GetCreateInstanceSupported(context); - } - - public override bool GetPropertiesSupported(ITypeDescriptorContext context) - { - return _inner.GetPropertiesSupported(context); - } - - public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) - { - return _inner.GetStandardValues(context); - } - - public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes) - { - return _inner.GetProperties(context, value, attributes); - } - - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - return _inner.CanConvertTo(context, destinationType); - } - - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return _inner.CanConvertFrom(context, sourceType); - } - - public override bool IsValid(ITypeDescriptorContext context, object value) - { - return _inner.IsValid(context, value); - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/ParseTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/ParseTypeConverter.cs deleted file mode 100644 index bfb446fa15..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/ParseTypeConverter.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; -using System.Reflection; - -namespace Avalonia.Markup.Xaml.Converters -{ - /// - /// Base class for type converters which call a static Parse method. - /// - public abstract class ParseTypeConverter : TypeConverter - { - protected const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; - protected static readonly Type[] StringParameter = new[] { typeof(string) }; - protected static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) }; - - /// - /// Checks whether a type has a suitable Parse method. - /// - /// The type. - /// True if the type has a suitable parse method, otherwise false. - public static bool HasParseMethod(Type type) - { - return type.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null) != null || - type.GetMethod("Parse", PublicStatic, null, StringParameter, null) != null; - } - } - - /// - /// A type converter which calls a static Parse method. - /// - /// The type with the Parse method. - public class ParseTypeConverter : ParseTypeConverter - { - private static Func _parse; - private static Func _parseWithFormat; - - static ParseTypeConverter() - { - var method = typeof(T).GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); - - if (method != null) - { - _parseWithFormat = (Func)method - .CreateDelegate(typeof(Func)); - return; - } - - method = typeof(T).GetMethod("Parse", PublicStatic, null, StringParameter, null); - - if (method != null) - { - _parse = (Func)method.CreateDelegate(typeof(Func)); - } - } - - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - if (value != null) - { - if (_parse != null) - { - return _parse(value.ToString()); - } - else if (_parseWithFormat != null) - { - return _parseWithFormat(value.ToString(), culture); - } - } - - return null; - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs deleted file mode 100644 index 54234fe406..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/SelectorTypeConverter.cs +++ /dev/null @@ -1,27 +0,0 @@ -// 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 System.Globalization; -using Avalonia.Markup.Parsers; - -namespace Avalonia.Markup.Xaml.Converters -{ - using Portable.Xaml.ComponentModel; - using System.ComponentModel; - - public class SelectorTypeConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - var parser = new SelectorParser(context.ResolveType); - - return parser.Parse((string)value); - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/SetterValueTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/SetterValueTypeConverter.cs deleted file mode 100644 index 81cda6db1f..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/SetterValueTypeConverter.cs +++ /dev/null @@ -1,49 +0,0 @@ -// 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.Styling; -using Portable.Xaml; -using Portable.Xaml.ComponentModel; -using System.ComponentModel; -using Portable.Xaml.Markup; -using System; -using System.Globalization; - -namespace Avalonia.Markup.Xaml.Converters -{ - public class SetterValueTypeConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - object setter = context.GetService().TargetObject; - var schemaContext = context.GetService().SchemaContext; - - return ConvertSetterValue(context, schemaContext, culture, (setter as Setter), value); - } - - [Obsolete("TODO: try assosiate Setter.Value property with SetterValueTypeConverter, so far coouldn't make it :(")] - internal static object ConvertSetterValue(ITypeDescriptorContext dcontext, XamlSchemaContext context, CultureInfo info, Setter setter, object value) - { - Type targetType = setter?.Property?.PropertyType; - - if (targetType == null) - { - return value; - } - - var ttConv = context.GetXamlType(targetType)?.TypeConverter?.ConverterInstance; - - if (ttConv != null) - { - value = ttConv.ConvertFromString(dcontext, info, value as string); - } - - return value; - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Extensions.cs b/src/Markup/Avalonia.Markup.Xaml/Extensions.cs index c6b914ba72..fe3fd44c1c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Extensions.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Extensions.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using Avalonia.Markup.Xaml.XamlIl.Runtime; -using Portable.Xaml; -using Portable.Xaml.Markup; namespace Avalonia.Markup.Xaml { @@ -13,42 +11,19 @@ namespace Avalonia.Markup.Xaml public static T GetService(this IServiceProvider sp) => (T)sp?.GetService(typeof(T)); - public static Uri GetContextBaseUri(this IServiceProvider ctx) - { - var properService = ctx.GetService(); - if (properService != null) - return properService.BaseUri; - // Ugly hack with casts - return Portable.Xaml.ComponentModel.TypeDescriptorExtensions.GetBaseUri((ITypeDescriptorContext)ctx); - } + public static Uri GetContextBaseUri(this IServiceProvider ctx) => ctx.GetService().BaseUri; - public static T GetFirstParent(this IServiceProvider ctx) where T : class - { - var parentStack = ctx.GetService(); - if (parentStack != null) - return parentStack.Parents.OfType().FirstOrDefault(); - return Portable.Xaml.ComponentModel.TypeDescriptorExtensions.GetFirstAmbientValue((ITypeDescriptorContext)ctx); - } - - public static T GetLastParent(this IServiceProvider ctx) where T : class - { - var parentStack = ctx.GetService(); - if (parentStack != null) - return parentStack.Parents.OfType().LastOrDefault(); - return Portable.Xaml.ComponentModel.TypeDescriptorExtensions.GetLastOrDefaultAmbientValue( - (ITypeDescriptorContext)ctx); - } + public static T GetFirstParent(this IServiceProvider ctx) where T : class + => ctx.GetService().Parents.OfType().FirstOrDefault(); + + public static T GetLastParent(this IServiceProvider ctx) where T : class + => ctx.GetService().Parents.OfType().LastOrDefault(); public static IEnumerable GetParents(this IServiceProvider sp) { - var stack = sp.GetService(); - if (stack != null) - return stack.Parents.OfType(); + return sp.GetService().Parents.OfType(); + - var context = (ITypeDescriptorContext)sp; - var schemaContext = context.GetService().SchemaContext; - var ambientProvider = context.GetService(); - return ambientProvider.GetAllAmbientValues(schemaContext.GetXamlType(typeof(T))).OfType(); } public static Type ResolveType(this IServiceProvider ctx, string namespacePrefix, string type) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtension.cs new file mode 100644 index 0000000000..858f2daa58 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtension.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Markup.Xaml +{ + public abstract class MarkupExtension + { + public abstract object ProvideValue(IServiceProvider serviceProvider); + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 223716ae3b..726f4221f8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -10,14 +10,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions using Avalonia.Data.Converters; using Avalonia.Markup.Data; using Avalonia.Styling; - using Portable.Xaml; - using Portable.Xaml.ComponentModel; - using Portable.Xaml.Markup; - using PortableXaml; using System.ComponentModel; - [MarkupExtensionReturnType(typeof(IBinding))] - public class BindingExtension : MarkupExtension + public class BindingExtension { public BindingExtension() { @@ -28,12 +23,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions Path = path; } - public override object ProvideValue(IServiceProvider serviceProvider) - { - return ProvideTypedValue(serviceProvider); - } - - public Binding ProvideTypedValue(IServiceProvider serviceProvider) + public Binding ProvideValue(IServiceProvider serviceProvider) { var descriptorContext = (ITypeDescriptorContext)serviceProvider; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 5f0e84c63a..0a9289bec9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -7,13 +7,10 @@ using System.Linq; using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Data; -using Portable.Xaml; -using Portable.Xaml.ComponentModel; -using Portable.Xaml.Markup; namespace Avalonia.Markup.Xaml.MarkupExtensions { - public class DynamicResourceExtension : MarkupExtension, IBinding + public class DynamicResourceExtension : IBinding { private IResourceNode _anchor; @@ -26,11 +23,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions ResourceKey = resourceKey; } - public string ResourceKey { get; set; } + public object ResourceKey { get; set; } - public override object ProvideValue(IServiceProvider serviceProvider) => ProvideTypedValue(serviceProvider); - - public IBinding ProvideTypedValue(IServiceProvider serviceProvider) + public IBinding ProvideValue(IServiceProvider serviceProvider) { var provideTarget = serviceProvider.GetService(); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs index 2f7256fa22..f690a5ff0e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs @@ -3,11 +3,10 @@ using System; using Avalonia.Data; -using Portable.Xaml.Markup; namespace Avalonia.Markup.Xaml.MarkupExtensions { - public class RelativeSourceExtension : MarkupExtension + public class RelativeSourceExtension { public RelativeSourceExtension() { @@ -18,7 +17,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions Mode = mode; } - public override object ProvideValue(IServiceProvider serviceProvider) + public RelativeSource ProvideValue(IServiceProvider serviceProvider) { return new RelativeSource { @@ -38,4 +37,4 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public int AncestorLevel { get; set; } = 1; } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 827f58a909..3525628a79 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -1,15 +1,13 @@ using System; using System.ComponentModel; using Avalonia.Controls; -using Portable.Xaml.ComponentModel; -using Portable.Xaml.Markup; namespace Avalonia.Markup.Xaml.MarkupExtensions { /// /// Loads a resource dictionary from a specified URL. /// - public class ResourceInclude : MarkupExtension, IResourceProvider + public class ResourceInclude :IResourceProvider { private Uri _baseUri; private IResourceDictionary _loaded; @@ -47,18 +45,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions bool IResourceProvider.HasResources => Loaded.HasResources; /// - bool IResourceProvider.TryGetResource(string key, out object value) + bool IResourceProvider.TryGetResource(object key, out object value) { return Loaded.TryGetResource(key, out value); } - /// - public override object ProvideValue(IServiceProvider serviceProvider) - { - return ProvideTypedValue(serviceProvider); - } - - public ResourceInclude ProvideTypedValue(IServiceProvider serviceProvider) + public ResourceInclude ProvideValue(IServiceProvider serviceProvider) { var tdc = (ITypeDescriptorContext)serviceProvider; _baseUri = tdc?.GetContextBaseUri(); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index ea913db598..d6b170ae9d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -7,13 +7,10 @@ using System.ComponentModel; using System.Reflection; using Avalonia.Controls; using Avalonia.Markup.Data; -using Portable.Xaml; -using Portable.Xaml.ComponentModel; -using Portable.Xaml.Markup; namespace Avalonia.Markup.Xaml.MarkupExtensions { - public class StaticResourceExtension : MarkupExtension + public class StaticResourceExtension { public StaticResourceExtension() { @@ -26,26 +23,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public string ResourceKey { get; set; } - public override object ProvideValue(IServiceProvider serviceProvider) + public object ProvideValue(IServiceProvider serviceProvider) { - - // Look upwards though the ambient context for IResourceProviders which might be able // to give us the resource. foreach (var resourceProvider in serviceProvider.GetParents()) { - // We override XamlType.CanAssignTo in BindingXamlType so the results we get back - // from GetAllAmbientValues aren't necessarily of the correct type. - - if (AvaloniaXamlLoader.UseLegacyXamlLoader - && resourceProvider is IControl control && control.StylingParent != null) - { - // If we've got to a control that has a StylingParent then it's probably - // a top level control and its StylingParent is pointing to the global - // styles. If this is case just do a FindResource on it. - return control.FindResource(ResourceKey); - } - else if (resourceProvider.TryGetResource(ResourceKey, out var value)) + if (resourceProvider.TryGetResource(ResourceKey, out var value)) { return value; } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleIncludeExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleIncludeExtension.cs index d9345738fc..a323050c31 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleIncludeExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleIncludeExtension.cs @@ -3,23 +3,18 @@ using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; -using Portable.Xaml; -using Portable.Xaml.ComponentModel; using System.ComponentModel; -using Portable.Xaml.Markup; using System; namespace Avalonia.Markup.Xaml.MarkupExtensions { - [MarkupExtensionReturnType(typeof(IStyle))] - public class StyleIncludeExtension : MarkupExtension + public class StyleIncludeExtension { public StyleIncludeExtension() { } - public override object ProvideValue(IServiceProvider serviceProvider) => ProvideTypedValue(serviceProvider); - public IStyle ProvideTypedValue(IServiceProvider serviceProvider) + public IStyle ProvideValue(IServiceProvider serviceProvider) { return new StyleInclude(serviceProvider.GetContextBaseUri()) { Source = Source }; } diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AttributeExtensions.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AttributeExtensions.cs deleted file mode 100644 index 2a23b7e068..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AttributeExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Avalonia.Markup.Xaml.Templates; -using avm = Avalonia.Metadata; -using pm = Portable.Xaml.Markup; - -namespace Avalonia.Markup.Xaml.PortableXaml -{ - internal static class AttributeExtensions - { - public static pm.XamlDeferLoadAttribute ToPortableXaml(this avm.TemplateContentAttribute attrib) - { - if (attrib == null) - { - return null; - } - - return new pm.XamlDeferLoadAttribute(typeof(TemplateLoader), typeof(TemplateContent)); - } - - public static pm.AmbientAttribute ToPortableXaml(this avm.AmbientAttribute attrib) - { - if (attrib == null) - { - return null; - } - - return new pm.AmbientAttribute(); - } - - public static pm.DependsOnAttribute ToPortableXaml(this avm.DependsOnAttribute attrib) - { - if (attrib == null) - { - return null; - } - - return new pm.DependsOnAttribute(attrib.Name); - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs deleted file mode 100644 index 529cbab938..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Avalonia.Markup.Xaml.Converters; -using Avalonia.Styling; -using Portable.Xaml.ComponentModel; -using System.ComponentModel; -using System; -using System.Linq; -using System.Reflection; -using avm = Avalonia.Metadata; -using pm = Portable.Xaml.Markup; - -namespace Avalonia.Markup.Xaml.PortableXaml -{ - public class AvaloniaMemberAttributeProvider : ICustomAttributeProvider - { - public AvaloniaMemberAttributeProvider(MemberInfo info) - { - _info = info; - } - - public object[] GetCustomAttributes(bool inherit) - { - throw new NotImplementedException(); - } - - public object[] GetCustomAttributes(Type attributeType, bool inherit) - { - Attribute result = null; - - if (attributeType == typeof(pm.XamlDeferLoadAttribute)) - { - result = _info.GetCustomAttribute(inherit) - .ToPortableXaml(); - } - else if (attributeType == typeof(pm.AmbientAttribute)) - { - result = _info.GetCustomAttribute(inherit) - .ToPortableXaml(); - } - else if (attributeType == typeof(pm.DependsOnAttribute)) - { - result = _info.GetCustomAttribute(inherit) - .ToPortableXaml(); - } - else if (attributeType == typeof(TypeConverterAttribute) && - _info.DeclaringType == typeof(Setter) && - _info.Name == nameof(Setter.Value)) - { - //actually it never comes here looks like if property type is object - //Portable.Xaml is not searching for Type Converter - result = new TypeConverterAttribute(typeof(SetterValueTypeConverter)); - } - else if (attributeType == typeof(TypeConverterAttribute) && _info is EventInfo) - { - // If a type converter for `EventInfo` is registered, then use that to convert - // event handler values. This is used by the designer to override the lookup - // for event handlers with a null handler. - var eventConverter = AvaloniaTypeConverters.GetTypeConverter(typeof(EventInfo)); - - if (eventConverter != null) - { - result = new TypeConverterAttribute(eventConverter); - } - } - - if (result == null) - { - var attr = _info.GetCustomAttributes(attributeType, inherit); - return (attr as object[]) ?? attr.ToArray(); - } - else - { - return new object[] { result }; - } - } - - public bool IsDefined(Type attributeType, bool inherit) - { - throw new NotImplementedException(); - } - - private readonly MemberInfo _info; - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaNameScope.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaNameScope.cs deleted file mode 100644 index 6f855bafa1..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaNameScope.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Controls; - -namespace Avalonia.Markup.Xaml.PortableXaml -{ - internal class AvaloniaNameScope : Portable.Xaml.Markup.INameScope - { - public object Instance { get; set; } - - private Dictionary _names = new Dictionary(); - - public object FindName(string name) - { - object result; - if (_names.TryGetValue(name, out result)) - return result; - return null; - } - - public void RegisterName(string name, object scopedElement) - { - if (scopedElement != null) - _names.Add(name, scopedElement); - - //TODO: ??? - //var control = scopedElement as Control; - - //if (control != null) - //{ - // var nameScope = (Instance as INameScope) ?? control.FindNameScope(); - - // if (nameScope != null) - // { - // nameScope.Register(name, scopedElement); - // } - //} - } - - public void UnregisterName(string name) - { - } - - public void RegisterOnNameScope(object target) - { - var nameScope = target as INameScope; - - if (nameScope != null) - { - foreach (var v in _names) - { - nameScope.Register(v.Key, v.Value); - } - } - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs deleted file mode 100644 index eb52e317b8..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs +++ /dev/null @@ -1,147 +0,0 @@ -// 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 System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Avalonia.Controls; -using Avalonia.Data; -using Avalonia.Markup.Xaml.Templates; -using Avalonia.Media; -using Avalonia.Metadata; -using Avalonia.Platform; -using Avalonia.Styling; - -namespace Avalonia.Markup.Xaml.Context -{ - using ClrNamespaceInfo = Tuple; - - public interface IRuntimeTypeProvider - { - Type FindType(string xamlNamespace, string name, Type[] genArgs); - - IEnumerable ReferencedAssemblies { get; } - } - - public class AvaloniaRuntimeTypeProvider : IRuntimeTypeProvider - { - private const string ClrNamespace = "clr-namespace:"; - // private const string AvaloniaNs = "https://github.com/avaloniaui"; - - private static readonly IEnumerable ForcedAssemblies = new[] - { - typeof(AvaloniaObject).GetTypeInfo().Assembly, - typeof(Animation.Animation).GetTypeInfo().Assembly, - typeof(Control).GetTypeInfo().Assembly, - typeof(Style).GetTypeInfo().Assembly, - typeof(DataTemplate).GetTypeInfo().Assembly, - typeof(SolidColorBrush).GetTypeInfo().Assembly, - typeof(Binding).GetTypeInfo().Assembly, - }; - - private Dictionary> _namespaces = new Dictionary>(); - - private List _scanned = new List(); - - public IEnumerable ReferencedAssemblies => _scanned; - - public AvaloniaRuntimeTypeProvider() - { - ScanAssemblies(ForcedAssemblies); - ScanNewAssemblies(); - } - - private static bool IsClrNamespace(string ns) - { - return ns.StartsWith(ClrNamespace); - } - - private static Assembly GetAssembly(string assemblyName) - { - return Assembly.Load(new AssemblyName(assemblyName)); - } - - private void ScanAssemblies(IEnumerable assemblies) - { - foreach (var assembly in assemblies) - { - var namespaces = assembly.GetCustomAttributes() - .Select(x => new { x.XmlNamespace, x.ClrNamespace }) - .GroupBy(x => x.XmlNamespace); - - foreach (var nsa in namespaces) - { - HashSet reg; - - if (!_namespaces.TryGetValue(nsa.Key, out reg)) - { - _namespaces[nsa.Key] = reg = new HashSet>(); - } - - foreach (var child in nsa) - { - reg.Add(new ClrNamespaceInfo(child.ClrNamespace, assembly)); - } - } - - _scanned.Add(assembly); - } - } - - private void ScanNewAssemblies() - { - IEnumerable assemblies = AppDomain.CurrentDomain.GetAssemblies(); - - if (assemblies != null) - { - assemblies = assemblies.Except(_scanned); - ScanAssemblies(assemblies); - } - } - - private Dictionary _typeCache = new Dictionary(); - - public Type FindType(string xamlNamespace, string name, Type[] genArgs) - { - if (IsClrNamespace(xamlNamespace)) - { - //we need to handle only xaml url namespaces for avalonia, - //the other namespaces are handled well in portable.xaml - return null; - } - - string key = $"{xamlNamespace}:{name}"; - - Type type; - - if (_typeCache.TryGetValue(key, out type)) - { - return type; - } - - HashSet reg; - - if (!_namespaces.TryGetValue(xamlNamespace, out reg)) - { - return null; - } - - if (genArgs != null) - name += "`" + genArgs.Length; - - foreach (var ns in reg) - { - var n = ns.Item1 + "." + name; - var t = ns.Item2.GetType(n); - if (t != null) - { - _typeCache[key] = t; - return t; - } - } - - return null; - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaTypeAttributeProvider.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaTypeAttributeProvider.cs deleted file mode 100644 index 7558a5df0b..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaTypeAttributeProvider.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Portable.Xaml.ComponentModel; -using System.ComponentModel; -using System; -using System.Linq; -using System.Reflection; -using avm = Avalonia.Metadata; -using pm = Portable.Xaml.Markup; - -namespace Avalonia.Markup.Xaml.PortableXaml -{ - internal class AvaloniaTypeAttributeProvider : ICustomAttributeProvider - { - public AvaloniaTypeAttributeProvider(Type type) - { - _type = type; - } - - public object[] GetCustomAttributes(bool inherit) - { - throw new NotImplementedException(); - } - - public object[] GetCustomAttributes(Type attributeType, bool inherit) - { - Attribute result = null; - - var ti = _type.GetTypeInfo(); - - if (attributeType == typeof(pm.ContentPropertyAttribute)) - { - result = GetContentPropertyAttribute(inherit); - } - else if (attributeType == typeof(pm.RuntimeNamePropertyAttribute)) - { - if (_namedType.IsAssignableFrom(ti)) - { - result = new pm.RuntimeNamePropertyAttribute(nameof(INamed.Name)); - } - } - else if (attributeType == typeof(TypeConverterAttribute)) - { - var builtin = AvaloniaTypeConverters.GetBuiltinTypeConverter(_type); - if (builtin != null) - result = new TypeConverterAttribute(builtin); - result = result ?? ti.GetCustomAttribute(attributeType, inherit); - - if (result == null) - { - var convType = AvaloniaTypeConverters.GetTypeConverter(_type); - - if (convType != null) - { - result = new TypeConverterAttribute(convType); - } - } - } - else if (attributeType == typeof(pm.AmbientAttribute)) - { - result = ti.GetCustomAttribute(inherit) - .ToPortableXaml(); - } - - if (result == null) - { - var attr = ti.GetCustomAttributes(attributeType, inherit); - return (attr as object[]) ?? attr.ToArray(); - } - else - { - return new object[] { result }; - } - } - - public bool IsDefined(Type attributeType, bool inherit) - { - throw new NotImplementedException(); - } - - private readonly TypeInfo _namedType = typeof(INamed).GetTypeInfo(); - - private readonly Type _type; - - private Attribute GetContentPropertyAttribute(bool inherit) - { - var type = _type; - - while (type != null) - { - var properties = type.GetTypeInfo().DeclaredProperties - .Where(x => x.GetCustomAttribute() != null); - string result = null; - - foreach (var property in properties) - { - if (result != null) - { - throw new Exception($"Content property defined more than once on {type}."); - } - - result = property.Name; - } - - if (result != null) - { - return new pm.ContentPropertyAttribute(result); - } - - type = inherit ? type.GetTypeInfo().BaseType : null; - } - - return null; - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlContext.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlContext.cs deleted file mode 100644 index c159b551b7..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlContext.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Portable.Xaml; -using Portable.Xaml.Markup; -using System; -using System.Reflection; - -namespace Avalonia.Markup.Xaml.PortableXaml -{ - public class AvaloniaXamlContext : IUriContext - { - private AvaloniaXamlContext() - { - } - - public Assembly LocalAssembly { get; private set; } - - public Uri BaseUri { get; set; } - - public object RootInstance { get; private set; } - - internal static AvaloniaXamlContext For(XamlXmlReaderSettings sett, - object rootInstance) - { - return new AvaloniaXamlContext() - { - BaseUri = sett.BaseUri, - LocalAssembly = sett.LocalAssembly, - RootInstance = rootInstance - }; - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs deleted file mode 100644 index 9fa6c26c35..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs +++ /dev/null @@ -1,222 +0,0 @@ -using Avalonia.Data; -using Portable.Xaml; -using Portable.Xaml.ComponentModel; -using System.ComponentModel; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Avalonia.Controls; -using Portable.Xaml.Schema; - -namespace Avalonia.Markup.Xaml.PortableXaml -{ - class AvaloniaXamlObjectWriter : XamlObjectWriter - { - private static Dictionary DesignDirectives = new Dictionary - { - ["DataContext"] = "DataContext", - ["DesignWidth"] = "Width", ["DesignHeight"] = "Height", ["PreviewWith"] = "PreviewWith" - } - .ToDictionary(p => new XamlDirective( - new[] {"http://schemas.microsoft.com/expression/blend/2008"}, p.Key, - XamlLanguage.Object, null, AllowedMemberLocations.Attribute), p => p.Value); - - private readonly AvaloniaXamlSchemaContext _schemaContext; - - public static AvaloniaXamlObjectWriter Create( - AvaloniaXamlSchemaContext schemaContext, - AvaloniaXamlContext context, - IAmbientProvider parentAmbientProvider = null) - { - var nameScope = new AvaloniaNameScope { Instance = context?.RootInstance }; - - var writerSettings = new XamlObjectWriterSettings() - { - ExternalNameScope = nameScope, - RegisterNamesOnExternalNamescope = true, - RootObjectInstance = context?.RootInstance - }; - - return new AvaloniaXamlObjectWriter(schemaContext, - writerSettings.WithContext(context), - nameScope, - parentAmbientProvider); - } - - private readonly DelayedValuesHelper _delayedValuesHelper = new DelayedValuesHelper(); - - private AvaloniaNameScope _nameScope; - - private AvaloniaXamlObjectWriter( - AvaloniaXamlSchemaContext schemaContext, - XamlObjectWriterSettings settings, - AvaloniaNameScope nameScope, - IAmbientProvider parentAmbientProvider) - : base(schemaContext, settings, parentAmbientProvider) - { - _nameScope = nameScope; - _schemaContext = schemaContext; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - if (_nameScope != null && Result != null) - { - _nameScope.RegisterOnNameScope(Result); - } - } - - base.Dispose(disposing); - } - - public void ApplyAllDelayedProperties() - { - //HACK: We need this because Begin/EndInit ordering is broken - _delayedValuesHelper.ApplyAll(); - } - - protected internal override void OnAfterProperties(object value) - { - _delayedValuesHelper.EndInit(value); - - base.OnAfterProperties(value); - } - - protected internal override void OnBeforeProperties(object value) - { - if (value != null) - _delayedValuesHelper.BeginInit(value); - - base.OnBeforeProperties(value); - } - - protected internal override bool OnSetValue(object target, XamlMember member, object value) - { - if (_delayedValuesHelper.TryAdd(target, member, value)) - { - return true; - } - - return base.OnSetValue(target, member, value); - } - - public override void WriteStartMember(XamlMember property) - { - foreach(var d in DesignDirectives) - if (property == d.Key && _schemaContext.IsDesignMode) - { - base.WriteStartMember(new XamlMember(d.Value, - typeof(Design).GetMethod("Get" + d.Value, BindingFlags.Static | BindingFlags.Public), - typeof(Design).GetMethod("Set" + d.Value, BindingFlags.Static | BindingFlags.Public), - SchemaContext)); - return; - } - base.WriteStartMember(property); - } - - private class DelayedValuesHelper - { - private int _cnt; - - private HashSet _targets = new HashSet(); - - private IList _values = new List(); - - private IEnumerable Values => _values; - - public void BeginInit(object target) - { - ++_cnt; - - AddTargetIfNeeded(target); - } - - public void EndInit(object target) - { - --_cnt; - - if (_cnt == 0) - { - ApplyAll(); - } - } - - public bool TryAdd(object target, XamlMember member, object value) - { - if (value is IBinding) - { - Add(new DelayedValue(target, member, value)); - - return true; - } - - return false; - } - - private void Add(DelayedValue value) - { - _values.Add(value); - - var target = value.Target; - - if (!_targets.Contains(value.Target)) - { - _targets.Add(target); - (target as ISupportInitialize)?.BeginInit(); - } - } - - private void AddTargetIfNeeded(object target) - { - if (!_targets.Contains(target)) - { - Add(new DelayedValue(target, null, null)); - } - } - - public void ApplyAll() - { - //TODO: revisit this - //apply delayed values and clear - //that's the last object let's set all delayed bindings - foreach (var dv in Values.Where(v => v.Member != null)) - { - dv.Member.Invoker.SetValue(dv.Target, dv.Value); - } - - //TODO: check/add some order of end init - //currently we are sending end init in the order of - //objects creation - foreach (var v in Values) - { - var target = v.Target; - - if (_targets.Contains(target)) - { - _targets.Remove(target); - (target as ISupportInitialize)?.EndInit(); - } - } - - _targets.Clear(); - _values.Clear(); - } - - private class DelayedValue - { - public DelayedValue(object target, XamlMember member, object value) - { - Target = target; - Member = member; - Value = value; - } - - public XamlMember Member { get; } - public object Target { get; } - public object Value { get; } - } - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs deleted file mode 100644 index 326db884a6..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs +++ /dev/null @@ -1,327 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Avalonia.Data; -using Avalonia.Markup.Xaml.Context; -using Avalonia.Markup.Xaml.MarkupExtensions; -using Avalonia.Markup.Xaml.Styling; -using Portable.Xaml; - -namespace Avalonia.Markup.Xaml.PortableXaml -{ - internal class AvaloniaXamlSchemaContext : XamlSchemaContext - { - private static AvaloniaXamlSchemaContext s_instance; - private static AvaloniaXamlSchemaContext s_designInstance; - - public static AvaloniaXamlSchemaContext Instance - { - get - { - if (s_instance == null) - { - s_instance = Create(); - } - - return s_instance; - } - } - - public static AvaloniaXamlSchemaContext DesignInstance - { - get - { - if (s_designInstance == null) - { - s_designInstance = Create(); - s_designInstance.IsDesignMode = true; - } - - return s_designInstance; - } - } - - public bool IsDesignMode { get; private set; } - public static AvaloniaXamlSchemaContext Create(IRuntimeTypeProvider typeProvider = null) - { - return new AvaloniaXamlSchemaContext(typeProvider ?? new AvaloniaRuntimeTypeProvider()); - } - - private AvaloniaXamlSchemaContext(IRuntimeTypeProvider typeProvider) - //better not set the references assemblies - //TODO: check this on iOS - //: base(typeProvider.ReferencedAssemblies) - { - Contract.Requires(typeProvider != null); - - _avaloniaTypeProvider = typeProvider; - } - - private IRuntimeTypeProvider _avaloniaTypeProvider; - - protected override XamlType GetXamlType(string xamlNamespace, string name, params XamlType[] typeArguments) - { - XamlType type = null; - try - { - type = ResolveXamlTypeName(xamlNamespace, name, typeArguments, false); - - if (type == null) - { - type = base.GetXamlType(xamlNamespace, name, typeArguments); - } - } - catch (Exception e) - { - //TODO: log or wrap exception - throw e; - } - return type; - } - - private XamlType ResolveXamlTypeName(string xmlNamespace, string xmlLocalName, XamlType[] typeArguments, bool required) - { - Type[] genArgs = null; - if (typeArguments != null && typeArguments.Any()) - { - genArgs = typeArguments.Select(t => t?.UnderlyingType).ToArray(); - - if (genArgs.Any(t => t == null)) - { - return null; - } - } - - // MarkupExtension type could omit "Extension" part in XML name. - Type type = _avaloniaTypeProvider.FindType(xmlNamespace, - xmlLocalName, - genArgs) ?? - _avaloniaTypeProvider.FindType(xmlNamespace, - xmlLocalName + "Extension", - genArgs); - - if (type != null) - { - Type extType; - if (_wellKnownExtensionTypes.TryGetValue(type, out extType)) - { - type = extType; - } - } - - if (type == null) - { - //let's try the simple types - //in Portable xaml like xmlns:sys='clr-namespace:System;assembly=mscorlib' - //and sys:Double is not resolved properly - return ResolveSimpleTypeName(xmlNamespace, xmlLocalName); - } - - return GetXamlType(type); - } - - #region Workaround for bug in Portablexaml system types like double,int etc ... - - private static Type[] _simpleTypes = new Type[] - { - typeof(bool), - typeof(byte), - typeof(char), - typeof(decimal), - typeof(double), - typeof(Int16), - typeof(Int32), - typeof(Int64), - typeof(float), - typeof(string), - typeof(TimeSpan), - typeof(Uri), - }; - - private static Dictionary, XamlType> _simpleXamlTypes; - - //in Portable xaml like xmlns:sys='clr-namespace:System;assembly=mscorlib' - //and sys:Double is not resolved properly - [Obsolete("TODO: remove once it's fixed in Portable.xaml")] - private static XamlType ResolveSimpleTypeName(string xmlNamespace, string xmlLocalName) - { - if (_simpleXamlTypes == null) - { - _simpleXamlTypes = new Dictionary, XamlType>(); - - foreach (var type in _simpleTypes) - { - string asmName = type.GetTypeInfo().Assembly.GetName().Name; - string ns = $"clr-namespace:{type.Namespace};assembly={asmName}"; - var xamlType = XamlLanguage.AllTypes.First(t => t.UnderlyingType == type); - _simpleXamlTypes.Add(new Tuple(ns, type.Name), xamlType); - } - } - - XamlType result; - - var key = new Tuple(xmlNamespace, xmlLocalName); - - _simpleXamlTypes.TryGetValue(key, out result); - - return result; - } - - #endregion Workaround for bug in Portablexaml system types like double,int etc ... - - protected internal override ICustomAttributeProvider GetCustomAttributeProvider(Type type) - => new AvaloniaTypeAttributeProvider(type); - - protected internal override ICustomAttributeProvider GetCustomAttributeProvider(MemberInfo member) - => new AvaloniaMemberAttributeProvider(member); - - public override XamlType GetXamlType(Type type) - { - XamlType result = null; - - if (_cachedTypes.TryGetValue(type, out result)) - { - return result; - } - - _cachedTypes[type] = result = GetAvaloniaXamlType(type) ?? base.GetXamlType(type); - - return result; - } - - private static readonly Dictionary _wellKnownExtensionTypes = new Dictionary() - { - { typeof(Binding), typeof(BindingExtension) }, - { typeof(StyleInclude), typeof(StyleIncludeExtension) }, - }; - - private XamlType GetAvaloniaXamlType(Type type) - { - //if type is extension get the original type to check - var origType = _wellKnownExtensionTypes.FirstOrDefault(v => v.Value == type).Key; - - if (typeof(IBinding).GetTypeInfo().IsAssignableFrom((origType ?? type).GetTypeInfo())) - { - return new BindingXamlType(type, this); - } - - if (origType != null || - typeof(AvaloniaObject).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) - { - return new AvaloniaXamlType(type, this); - } - - return null; - } - - protected internal override XamlMember GetAttachableProperty(string attachablePropertyName, MethodInfo getter, MethodInfo setter) - { - var key = MemberKey.Create(getter ?? setter, attachablePropertyName, "a"); - - XamlMember result; - - if (_cachedMembers.TryGetValue(key, out result)) - { - return result; - } - - var type = (getter ?? setter).DeclaringType; - - var prop = AvaloniaPropertyRegistry.Instance.FindRegistered(type, attachablePropertyName); - - if (prop != null) - { - result = new AvaloniaAttachedPropertyXamlMember( - prop, attachablePropertyName, - getter, setter, this); - } - - if (result == null) - { - result = base.GetAttachableProperty(attachablePropertyName, getter, setter); - } - - return _cachedMembers[key] = result; - } - - protected internal override XamlMember GetProperty(PropertyInfo pi) - { - Type objType = pi.DeclaringType; - string name = pi.Name; - - XamlMember result; - - var key = MemberKey.Create(pi, "p"); - - if (_cachedMembers.TryGetValue(key, out result)) - { - return result; - } - - var avProp = AvaloniaPropertyRegistry.Instance.FindRegistered(objType, name); - - if (avProp != null) - { - result = new AvaloniaPropertyXamlMember(avProp, pi, this); - } - - if (result == null) - { - result = new PropertyXamlMember(pi, this); - } - - return _cachedMembers[key] = result; - } - - private Dictionary _cachedTypes = new Dictionary(); - - private Dictionary _cachedMembers = new Dictionary(); - - private struct MemberKey - { - public static MemberKey Create(MemberInfo m, string name, string memberType) - { - return new MemberKey(m.DeclaringType, name, memberType); - } - - public static MemberKey Create(MemberInfo m, string memberType) - { - return Create(m, m.Name, memberType); - } - - public MemberKey(Type type, object member, string memberType) - { - Type = type; - Member = member; - MemberType = memberType; - } - - public Type Type { get; } - - public object Member { get; } - - public string MemberType { get; } - - public override string ToString() - { - return $"{MemberType}:{Type.Namespace}:{Type.Name}.{Member}"; - } - } - - - public override bool TryGetCompatibleXamlNamespace(string xamlNamespace, out string compatibleNamespace) - { - //Forces XamlXmlReader to not ignore our namespace in design mode if mc:Ignorable is set - if (IsDesignMode && - xamlNamespace == "http://schemas.microsoft.com/expression/blend/2008") - { - compatibleNamespace = xamlNamespace; - return true; - } - - return base.TryGetCompatibleXamlNamespace(xamlNamespace, out compatibleNamespace); - } - - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs deleted file mode 100644 index 10cf716912..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs +++ /dev/null @@ -1,388 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Data; -using Avalonia.Markup.Data; -using Avalonia.Metadata; -using Avalonia.Styling; -using Portable.Xaml; -using Portable.Xaml.Schema; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Reflection; -using System.Xml.Serialization; - -namespace Avalonia.Markup.Xaml.PortableXaml -{ - using Converters; - using PropertyKey = Tuple; - - public class AvaloniaXamlType : XamlType - { - static readonly AvaloniaPropertyTypeConverter propertyTypeConverter = new AvaloniaPropertyTypeConverter(); - - public AvaloniaXamlType(Type underlyingType, XamlSchemaContext schemaContext) : - base(underlyingType, schemaContext) - { - } - - protected override XamlMember LookupAttachableMember(string name) - { - var m = base.LookupAttachableMember(name); - - if (m == null) - { - // Might be an AddOwnered attached property. - var avProp = AvaloniaPropertyRegistry.Instance.FindRegistered(UnderlyingType, name); - - if (avProp?.IsAttached == true) - { - return new AvaloniaPropertyXamlMember(avProp, this); - } - } - - return m; - } - - protected override XamlMember LookupMember(string name, bool skipReadOnlyCheck) - { - var m = base.LookupMember(name, skipReadOnlyCheck); - - if (m == null && !name.Contains(".")) - { - //so far Portable.xaml haven't found the member/property - //but what if we have AvaloniaProperty - //without setter and/or without getter - //let's try to find the AvaloniaProperty as a fallback - var avProp = AvaloniaPropertyRegistry.Instance.FindRegistered(UnderlyingType, name); - - if (avProp != null && !(skipReadOnlyCheck && avProp.IsReadOnly)) - { - m = new AvaloniaPropertyXamlMember(avProp, this); - } - } - - return m; - } - } - - public class BindingXamlType : XamlType - { - private static List _notAssignable = - new List - { - typeof (IXmlSerializable) - }; - - public BindingXamlType(Type underlyingType, XamlSchemaContext schemaContext) : - base(underlyingType, schemaContext) - { - } - - public override bool CanAssignTo(XamlType xamlType) - { - return !_notAssignable.Contains(xamlType.UnderlyingType); - } - } - - public class PropertyXamlMember : XamlMember - { - public PropertyXamlMember(PropertyInfo propertyInfo, XamlSchemaContext schemaContext) - : base(propertyInfo, schemaContext) - { - } - - protected PropertyXamlMember(string attachablePropertyName, - MethodInfo getter, MethodInfo setter, XamlSchemaContext schemaContext) - : base(attachablePropertyName, getter, setter, schemaContext) - { - } - - protected PropertyXamlMember(string name, XamlType declaringType, bool isAttachable) - : base(name, declaringType, isAttachable) - { - } - - private bool IsReadOnlyCollectionProperty - { - get - { - //Collection properties like: - //MultiBinding.Bindings, Panel.Children, Control.Styles, - //need to be readonly for Portable.Xaml - //Collection properties like: - //Grid.RowDefinitions, Grid.ColumnDefinitions - //need to be set only once, and subsequent changes to be - //added to collection - //TODO: investigate is this good enough as solution ??? - //We can add some ReadOnyXamlPropertyCollectionAttribute to cover this - return Type.IsCollection; - } - } - - private bool HasCollectionTypeConverter - { - get - { - return Type.IsCollection && Type.TypeConverter != null; - } - } - - protected override MethodInfo LookupUnderlyingSetter() - { - //if we have content property a list - //we have some issues in portable.xaml - //but if the list is read only, this is solving the problem - - if (IsReadOnlyCollectionProperty && - !HasCollectionTypeConverter) - { - return null; - } - - return base.LookupUnderlyingSetter(); - } - - protected override XamlMemberInvoker LookupInvoker() - { - //if we have a IList property and it has TypeConverter - //Portable.xaml need to be able to set the value - //but instead directly set new value we'll sync the lists - bool updateListInsteadSet = HasCollectionTypeConverter; - return new PropertyInvoker(this) - { - UpdateListInsteadSet = updateListInsteadSet - }; - } - - protected override bool LookupIsUnknown() => false; - - protected override XamlType LookupType() - { - var propType = GetPropertyType(); - - if (propType != null) - { - if (propType == typeof(IEnumerable)) - { - //TODO: Portable.xaml is not handling well IEnumerable - //let's threat IEnumerable property as list - //revisit this when smarter solution is found - propType = typeof(IList); - } - - return DeclaringType.SchemaContext.GetXamlType(propType); - } - - return base.LookupType(); - } - - protected virtual Type GetPropertyType() - { - return (UnderlyingMember as PropertyInfo)?.PropertyType; - } - - private IList _dependsOn; - - protected override IList LookupDependsOn() - { - if (_dependsOn == null) - { - var attrib = UnderlyingMember.GetCustomAttribute(true); - - if (attrib != null) - { - var member = DeclaringType.GetMember(attrib.Name); - - _dependsOn = new XamlMember[] { member }; - } - else - { - _dependsOn = base.LookupDependsOn(); - } - } - - return _dependsOn; - } - - private PropertyKey PropertyKey() - => new PropertyKey(DeclaringType.UnderlyingType, Name); - - private class PropertyInvoker : XamlMemberInvoker - { - public bool UpdateListInsteadSet { get; set; } = false; - - public PropertyInvoker(XamlMember member) : base(member) - { - } - - public override void SetValue(object instance, object value) - { - //can't make it work to assign TypeConverter to Setter.Value - //so we need it hard coded - //TODO: try to assosiate TypeConverter with Setter.Value - //and remove this lines - if (instance is Setter && - Member.Name == nameof(Setter.Value) && - value is string) - { - value = SetterValueTypeConverter.ConvertSetterValue(null, - Member.DeclaringType.SchemaContext, CultureInfo.InvariantCulture, - instance as Setter, - value); - } - - if (UpdateListInsteadSet && - value != null && - UpdateListInsteadSetValue(instance, value)) - { - return; - } - - base.SetValue(instance, value); - } - - private bool UpdateListInsteadSetValue(object instance, object value) - { - object old = GetValue(instance); - - if (Equals(old, value)) - { - //don't set the same collection value - return true; - } - else if (old is IList && value is IList) - { - var oldList = (IList)old; - var curList = (IList)value; - - oldList.Clear(); - - foreach (object item in curList) - { - oldList.Add(item); - } - - return true; - } - - return false; - } - } - } - - public class AvaloniaPropertyXamlMember : PropertyXamlMember - { - private bool? _assignBinding; - - public bool AssignBinding => (bool)(_assignBinding ?? (_assignBinding = UnderlyingMember?.GetCustomAttribute() != null)); - - public AvaloniaProperty Property { get; } - - public AvaloniaPropertyXamlMember(AvaloniaProperty property, - PropertyInfo propertyInfo, - XamlSchemaContext schemaContext) : - base(propertyInfo, schemaContext) - { - Property = property; - } - - public AvaloniaPropertyXamlMember(AvaloniaProperty property, XamlType type) : - base(property.Name, type, false) - { - Property = property; - } - - protected AvaloniaPropertyXamlMember(AvaloniaProperty property, - string attachablePropertyName, - MethodInfo getter, MethodInfo setter, XamlSchemaContext schemaContext) - : base(attachablePropertyName, getter, setter, schemaContext) - { - Property = property; - } - - protected override XamlMemberInvoker LookupInvoker() - { - return new AvaloniaPropertyInvoker(this); - } - - protected override bool LookupIsReadOnly() - { - return Property.IsReadOnly; - } - - protected override Type GetPropertyType() - { - return Property.PropertyType; - } - - private class AvaloniaPropertyInvoker : XamlMemberInvoker - { - public AvaloniaPropertyInvoker(XamlMember member) : base(member) - { - } - - public override void SetValue(object instance, object value) - { - if (Property != null) - { - var obj = ((IAvaloniaObject)instance); - if (value is IBinding) - { - if (!Member.AssignBinding) - ApplyBinding(obj, (IBinding)value); - else - obj.SetValue(Property, value); - } - else - { - obj.SetValue(Property, value); - } - } - else - { - base.SetValue(instance, value); - } - } - - public override object GetValue(object instance) - { - if (Property != null && !Property.IsAttached) - { - return ((IAvaloniaObject)instance).GetValue(Property); - } - else - { - return base.GetValue(instance); - } - } - - private void ApplyBinding(IAvaloniaObject obj, IBinding binding) - { - var control = obj as IControl; - var property = Property; - - if (control != null && property != Control.DataContextProperty) - DelayedBinding.Add(control, property, binding); - else - obj.Bind(property, binding); - } - - private AvaloniaProperty Property => Member.Property; - - private new AvaloniaPropertyXamlMember Member => - (AvaloniaPropertyXamlMember)base.Member; - } - } - - public class AvaloniaAttachedPropertyXamlMember : AvaloniaPropertyXamlMember - { - public AvaloniaAttachedPropertyXamlMember(AvaloniaProperty property, - string attachablePropertyName, - MethodInfo getter, MethodInfo setter, - XamlSchemaContext schemaContext) - : base(property, attachablePropertyName, getter, setter, schemaContext) - { - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/TypeDescriptorExtensions.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/TypeDescriptorExtensions.cs deleted file mode 100644 index a48f400af2..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/TypeDescriptorExtensions.cs +++ /dev/null @@ -1,101 +0,0 @@ -using Avalonia.Markup.Xaml.PortableXaml; -using Portable.Xaml.Markup; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.ComponentModel; - -namespace Portable.Xaml.ComponentModel -{ - internal static class TypeDescriptorExtensions - { - /// - /// Gets the service from ITypeDescriptorContext - /// usually in TypeConverter in xaml reader context - /// examples: - /// context.GetService<IXamlTypeResolver>() - /// context.GetService<IXamlNamespaceResolver>() - /// context.GetService<IXamlNameProvider>() - /// context.GetService<INamespacePrefixLookup>() - /// context.GetService<IXamlSchemaContextProvider>() - /// context.GetService<IRootObjectProvider>() - /// context.GetService<IProvideValueTarget>() - /// - /// Service Type - /// The TypeDescriptor context. - /// - public static T GetService(this ITypeDescriptorContext ctx) where T : class - { - return ctx.GetService(typeof(T)) as T; - } - - public static Type ResolveType(this ITypeDescriptorContext ctx, string namespacePrefix, string type) - { - var tr = ctx.GetService(); - - string name = string.IsNullOrEmpty(namespacePrefix) ? type : $"{namespacePrefix}:{type}"; - - return tr?.Resolve(name); - } - - public static T GetFirstAmbientValue(this ITypeDescriptorContext ctx) where T : class - { - var amb = ctx.GetService(); - var sc = ctx.GetService().SchemaContext; - - // Because GetFirstParent uses XamlType.CanAssignTo it returns values that - // aren't actually of the correct type. Use GetAllAmbientValues instead. - return amb.GetAllAmbientValues(sc.GetXamlType(typeof(T))).OfType().FirstOrDefault(); - } - - public static T GetLastOrDefaultAmbientValue(this ITypeDescriptorContext ctx) where T : class - { - return ctx.GetAllAmbientValues().LastOrDefault() as T; - } - - public static IEnumerable GetAllAmbientValues(this ITypeDescriptorContext ctx) where T : class - { - var amb = ctx.GetService(); - var sc = ctx.GetService().SchemaContext; - - return amb.GetAllAmbientValues(sc.GetXamlType(typeof(T))).OfType(); - } - - public static Uri GetBaseUri(this ITypeDescriptorContext ctx) - { - return ctx.GetWriterSettings()?.Context?.BaseUri; - } - - public static Assembly GetLocalAssembly(this ITypeDescriptorContext ctx) - { - return ctx.GetWriterSettings()?.Context?.LocalAssembly; - } - - public static AvaloniaXamlContext GetAvaloniaXamlContext(this ITypeDescriptorContext ctx) - { - return ctx.GetWriterSettings()?.Context; - } - - public static XamlObjectWriterSettings WithContext(this XamlObjectWriterSettings settings, AvaloniaXamlContext context) - { - return new AvaloniaXamlObjectWriterSettings(settings, context); - } - - private static AvaloniaXamlObjectWriterSettings GetWriterSettings(this ITypeDescriptorContext ctx) - { - return ctx.GetService().GetParentSettings() as AvaloniaXamlObjectWriterSettings; - } - - private class AvaloniaXamlObjectWriterSettings : XamlObjectWriterSettings - { - public AvaloniaXamlObjectWriterSettings(XamlObjectWriterSettings settings, AvaloniaXamlContext context) - : base(settings) - { - Context = context; - } - - public AvaloniaXamlContext Context { get; } - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github deleted file mode 160000 index ab55261737..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ab5526173722b8988bc5ca3c03c8752ce89c0975 diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 01ec9753bd..7acee50d80 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -86,7 +86,7 @@ namespace Avalonia.Markup.Xaml.Styling } /// - public bool TryGetResource(string key, out object value) => Loaded.TryGetResource(key, out value); + public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value); /// void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index ff367c0e13..ead373d380 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -7,42 +7,16 @@ using System.Collections.Generic; namespace Avalonia.Markup.Xaml.Templates { - using Portable.Xaml; - - public class TemplateContent + + public static class TemplateContent { - public TemplateContent(IEnumerable namespaces, XamlReader reader, - IAmbientProvider ambientProvider) - { - ParentAmbientProvider = ambientProvider; - List = new XamlNodeList(reader.SchemaContext); - - //we need to rpeserve all namespace and prefixes to writer - //otherwise they are lost. a bug in Portable.xaml or by design ?? - foreach (var ns in namespaces) - { - List.Writer.WriteNamespace(ns); - } - - XamlServices.Transform(reader, List.Writer); - } - - public XamlNodeList List { get; } - - private IAmbientProvider ParentAmbientProvider { get; } - - public IControl Load() - { - return (IControl)AvaloniaXamlLoader.LoadFromReader(List.GetReader(), parentAmbientProvider: ParentAmbientProvider); - } - public static IControl Load(object templateContent) { if (templateContent is Func direct) { return (IControl)direct(null); } - return ((TemplateContent)templateContent).Load(); + throw new ArgumentException(nameof(templateContent)); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateLoader.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateLoader.cs deleted file mode 100644 index e29485ddb0..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateLoader.cs +++ /dev/null @@ -1,26 +0,0 @@ -// 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.Markup.Xaml.Templates -{ - using Portable.Xaml; - using Portable.Xaml.ComponentModel; - using System.ComponentModel; - using System; - - public class TemplateLoader : XamlDeferringLoader - { - public override object Load(XamlReader xamlReader, IServiceProvider serviceProvider) - { - var tdc = (ITypeDescriptorContext)serviceProvider; - var ns = tdc.GetService(); - var ambientProvider = tdc.GetService(); - return new TemplateContent(ns.GetNamespacePrefixes(), xamlReader, ambientProvider); - } - - public override XamlReader Save(object value, IServiceProvider serviceProvider) - { - return ((TemplateContent)value).List.GetReader(); - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 65149a65de..b84f50fa8d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -112,13 +112,31 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions rootType = new XamlIlAstClrTypeReference(rootObject, overrideRootType, false); } - rootObject.Type = rootType; + OverrideRootType(parsed, rootType); Transform(parsed); Compile(parsed, tb, _contextType, PopulateName, BuildName, "__AvaloniaXamlIlNsInfo", baseUri, fileSource); } - - + + public void OverrideRootType(XamlIlDocument doc, IXamlIlAstTypeReference newType) + { + var root = (XamlIlAstObjectNode)doc.Root; + var oldType = root.Type; + if (oldType.Equals(newType)) + return; + + root.Type = newType; + foreach (var child in root.Children.OfType()) + { + if (child.Property is XamlIlAstNamePropertyReference prop) + { + if (prop.DeclaringType.Equals(oldType)) + prop.DeclaringType = newType; + if (prop.TargetType.Equals(oldType)) + prop.TargetType = newType; + } + } + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index 581dbcdac7..c25e1186d0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -28,15 +28,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XmlnsAttributes = { typeSystem.GetType("Avalonia.Metadata.XmlnsDefinitionAttribute"), - typeSystem.FindType("Portable.Xaml.Markup.XmlnsDefinitionAttribute") }, ContentAttributes = { typeSystem.GetType("Avalonia.Metadata.ContentAttribute") }, - ProvideValueTarget = typeSystem.GetType("Portable.Xaml.Markup.IProvideValueTarget"), - RootObjectProvider = typeSystem.GetType("Portable.Xaml.IRootObjectProvider"), - UriContextProvider = typeSystem.GetType("Portable.Xaml.Markup.IUriContext"), + ProvideValueTarget = typeSystem.GetType("Avalonia.Markup.Xaml.IProvideValueTarget"), + RootObjectProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IRootObjectProvider"), + UriContextProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IUriContext"), ParentStackProvider = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlParentStackProvider"), @@ -47,7 +46,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV1"), UsableDuringInitializationAttributes = { - typeSystem.GetType("Portable.Xaml.Markup.UsableDuringInitializationAttribute"), typeSystem.GetType("Avalonia.Metadata.UsableDuringInitializationAttribute"), }, InnerServiceProviderFactoryMethod = @@ -79,15 +77,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions void Add(string type, string conv) => AddType(typeSystem.GetType(type), typeSystem.GetType(conv)); - - //Add("Avalonia.AvaloniaProperty","Avalonia.Markup.Xaml.Converters.AvaloniaPropertyTypeConverter"); Add("Avalonia.Media.Imaging.IBitmap","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); var ilist = typeSystem.GetType("System.Collections.Generic.IList`1"); AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); Add("Avalonia.Controls.Templates.IMemberSelector", "Avalonia.Markup.Xaml.Converters.MemberSelectorTypeConverter"); - Add("Avalonia.Styling.Selector","Avalonia.Markup.Xaml.Converters.SelectorTypeConverter"); Add("Avalonia.Controls.WindowIcon","Avalonia.Markup.Xaml.Converters.IconTypeConverter"); Add("System.Globalization.CultureInfo", "System.ComponentModel.CultureInfoConverter"); Add("System.Uri", "Avalonia.Markup.Xaml.Converters.AvaloniaUriTypeConverter"); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs index 0e7ea34bb7..629e2562d3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs @@ -16,8 +16,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (!(node is XamlIlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.Setter")) return node; + var parent = context.ParentNodes().OfType() - .FirstOrDefault(x => x.Type.GetClrType().FullName == "Avalonia.Styling.Style"); + .FirstOrDefault(p => p.Type.GetClrType().FullName == "Avalonia.Styling.Style"); + if (parent == null) throw new XamlIlParseException( "Avalonia.Styling.Setter is only valid inside Avalonia.Styling.Style", node); @@ -53,8 +55,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers .OfType().FirstOrDefault(p => p.Property.GetClrProperty().Name == "Value"); if (valueProperty?.Values?.Count == 1 && valueProperty.Values[0] is XamlIlAstTextNode) { - var propType = avaloniaPropertyNode.Property.Getter?.ReturnType - ?? avaloniaPropertyNode.Property.Setters.First().Parameters[0]; + var propType = avaloniaPropertyNode.AvaloniaPropertyType; if (!XamlIlTransformHelpers.TryGetCorrectlyTypedValue(context, valueProperty.Values[0], propType, out var converted)) throw new XamlIlParseException( diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index b57c26c241..c054e57380 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -10,6 +10,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlIlType BindingPriority { get; } public IXamlIlType AvaloniaObjectExtensions { get; } public IXamlIlType AvaloniaProperty { get; } + public IXamlIlType AvaloniaPropertyT { get; } public IXamlIlType IBinding { get; } public IXamlIlMethod AvaloniaObjectBindMethod { get; } public IXamlIlMethod AvaloniaObjectSetValueMethod { get; } @@ -26,6 +27,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers IAvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.IAvaloniaObject"); AvaloniaObjectExtensions = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions"); AvaloniaProperty = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty"); + AvaloniaPropertyT = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty`1"); BindingPriority = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.BindingPriority"); IBinding = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.IBinding"); IDisposable = ctx.Configuration.TypeSystem.GetType("System.IDisposable"); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index 452bb05132..6fc83cb5a5 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -44,7 +44,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return true; } - public static XamlIlAvaloniaPropertyNode CreateNode(XamlIlAstTransformationContext context, + public static IXamlIlAvaloniaPropertyNode CreateNode(XamlIlAstTransformationContext context, string propertyName, IXamlIlAstTypeReference selectorTypeReference, IXamlIlLineInfo lineInfo) { XamlIlAstNamePropertyReference forgedReference; @@ -63,8 +63,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions xmlOwner += parsedPropertyName.owner; var tref = XamlIlTypeReferenceResolver.ResolveType(context, xmlOwner, false, lineInfo, true); - forgedReference = new XamlIlAstNamePropertyReference(lineInfo, - tref, parsedPropertyName.name, tref); + + var propertyFieldName = parsedPropertyName.name + "Property"; + var found = tref.Type.GetAllFields() + .FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == propertyFieldName); + if (found == null) + throw new XamlIlParseException( + $"Unable to find {propertyFieldName} field on type {tref.Type.GetFullName()}", lineInfo); + return new XamlIlAvaloniaPropertyFieldNode(context.GetAvaloniaTypes(), lineInfo, found); } var clrProperty = @@ -75,13 +81,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions clrProperty); } } + + interface IXamlIlAvaloniaPropertyNode : IXamlIlAstValueNode + { + IXamlIlType AvaloniaPropertyType { get; } + } - class XamlIlAvaloniaPropertyNode : XamlIlAstNode, IXamlIlAstValueNode, IXamlIlAstEmitableNode + class XamlIlAvaloniaPropertyNode : XamlIlAstNode, IXamlIlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode { public XamlIlAvaloniaPropertyNode(IXamlIlLineInfo lineInfo, IXamlIlType type, XamlIlAstClrProperty property) : base(lineInfo) { Type = new XamlIlAstClrTypeReference(this, type, false); Property = property; + AvaloniaPropertyType = Property.Getter?.ReturnType + ?? Property.Setters.First().Parameters[0]; } public XamlIlAstClrProperty Property { get; } @@ -93,6 +106,46 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions throw new XamlIlLoadException(Property.Name + " is not an AvaloniaProperty", this); return XamlIlNodeEmitResult.Type(0, Type.GetClrType()); } + + public IXamlIlType AvaloniaPropertyType { get; } + } + + class XamlIlAvaloniaPropertyFieldNode : XamlIlAstNode, IXamlIlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode + { + private readonly IXamlIlField _field; + + public XamlIlAvaloniaPropertyFieldNode(AvaloniaXamlIlWellKnownTypes types, + IXamlIlLineInfo lineInfo, IXamlIlField field) : base(lineInfo) + { + _field = field; + var avaloniaPropertyType = field.FieldType; + while (avaloniaPropertyType != null) + { + if (avaloniaPropertyType.GenericTypeDefinition?.Equals(types.AvaloniaPropertyT) == true) + { + AvaloniaPropertyType = avaloniaPropertyType.GenericArguments[0]; + return; + } + + avaloniaPropertyType = avaloniaPropertyType.BaseType; + } + + throw new XamlIlParseException( + $"{field.Name}'s type {field.FieldType} doesn't inherit from AvaloniaProperty, make sure to use typed properties", + lineInfo); + + } + + + + public IXamlIlAstTypeReference Type => new XamlIlAstClrTypeReference(this, _field.FieldType, false); + public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.Ldsfld(_field); + return XamlIlNodeEmitResult.Type(0, _field.FieldType); + } + + public IXamlIlType AvaloniaPropertyType { get; } } interface IXamlIlAvaloniaProperty diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index f91e221ac0..2d8ea643ac 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Reflection; using Avalonia.Controls; using Avalonia.Data; -using Portable.Xaml.Markup; // ReSharper disable UnusedMember.Global // ReSharper disable UnusedParameter.Global @@ -17,19 +16,24 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { var resourceNodes = provider.GetService().Parents .OfType().ToList(); - - return sp => builder(new DeferredParentServiceProvider(sp, resourceNodes)); + var rootObject = provider.GetService().RootObject; + return sp => builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject)); } - class DeferredParentServiceProvider : IAvaloniaXamlIlParentStackProvider, IServiceProvider + class DeferredParentServiceProvider : + IAvaloniaXamlIlParentStackProvider, + IServiceProvider, + IRootObjectProvider { private readonly IServiceProvider _parentProvider; private readonly List _parentResourceNodes; - public DeferredParentServiceProvider(IServiceProvider parentProvider, List parentResourceNodes) + public DeferredParentServiceProvider(IServiceProvider parentProvider, List parentResourceNodes, + object rootObject) { _parentProvider = parentProvider; _parentResourceNodes = parentResourceNodes; + RootObject = rootObject; } public IEnumerable Parents => GetParents(); @@ -46,8 +50,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider)) return this; + if (serviceType == typeof(IRootObjectProvider)) + return this; return _parentProvider?.GetService(serviceType); } + + public object RootObject { get; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 3b3c1f93a5..610cda30c6 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 3b3c1f93a566080d417b9782f9cc4ea67cd62344 +Subproject commit 610cda30c69e32e83c8235060606480904c937bc diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs new file mode 100644 index 0000000000..06cc85101a --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs @@ -0,0 +1,34 @@ +using System; + +namespace Avalonia.Markup.Xaml +{ + public interface IProvideValueTarget + { + object TargetObject { get; } + object TargetProperty { get; } + } + + public interface IRootObjectProvider + { + object RootObject { get; } + } + + public interface IUriContext + { + Uri BaseUri { get; set; } + } + + public interface IXamlTypeResolver + { + Type Resolve (string qualifiedTypeName); + } + + + public class ConstructorArgumentAttribute : Attribute + { + public ConstructorArgumentAttribute(string name) + { + + } + } +} diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 0479a74937..4f884cdf33 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -3,6 +3,7 @@ netstandard2.0 Avalonia.Skia Avalonia.Skia + Avalonia.Skia true true diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index b701e60660..eb7b65cdce 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -28,7 +28,7 @@ namespace Avalonia.Skia // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); - SKTypeface skiaTypeface = TypefaceCache.Default; + SKTypeface skiaTypeface = null; if (typeface.FontFamily.Key != null) { @@ -45,7 +45,7 @@ namespace Avalonia.Skia familyName, typeface.Style, typeface.Weight); - if (skiaTypeface != TypefaceCache.Default) break; + if (skiaTypeface.FamilyName != TypefaceCache.DefaultFamilyName) break; } } else diff --git a/src/Skia/Avalonia.Skia/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/GlRenderTarget.cs index 7c0c42ca37..a7c1d0a38b 100644 --- a/src/Skia/Avalonia.Skia/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/GlRenderTarget.cs @@ -8,7 +8,7 @@ using static Avalonia.OpenGL.GlConsts; namespace Avalonia.Skia { - internal class GlRenderTarget : IRenderTarget + internal class GlRenderTarget : IRenderTargetWithCorruptionInfo { private readonly GRContext _grContext; private IGlPlatformSurfaceRenderTarget _surface; @@ -21,6 +21,8 @@ namespace Avalonia.Skia public void Dispose() => _surface.Dispose(); + public bool IsCorrupted => (_surface as IGlPlatformSurfaceRenderTargetWithCorruptionInfo)?.IsCorrupted == true; + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) { var session = _surface.BeginDraw(); diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 362fd028cf..15f38b1c4f 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -111,8 +111,7 @@ namespace Avalonia.Skia Width = size.Width, Height = size.Height, Dpi = dpi, - DisableTextLcdRendering = false, - GrContext = GrContext + DisableTextLcdRendering = false }; return new SurfaceRenderTarget(createInfo); diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 17c51dbb6e..17448127b0 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -47,7 +47,7 @@ namespace Avalonia.Skia if (!_fontFamilies.TryGetValue(typeface.FontFamily.Name, out var fontFamily)) { - return TypefaceCache.Default; + return TypefaceCache.GetTypeface(TypefaceCache.DefaultFamilyName, typeface.Style, typeface.Weight); } var weight = (SKFontStyleWeight)typeface.Weight; diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs index 94b9e89962..9e270114d2 100644 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ b/src/Skia/Avalonia.Skia/TypefaceCache.cs @@ -12,8 +12,10 @@ namespace Avalonia.Skia /// internal static class TypefaceCache { - public static SKTypeface Default = CreateDefaultTypeface(); - static readonly Dictionary> Cache = new Dictionary>(); + public static readonly string DefaultFamilyName = CreateDefaultFamilyName(); + + private static readonly Dictionary> s_cache = + new Dictionary>(); struct FontKey { @@ -49,26 +51,26 @@ namespace Avalonia.Skia // Equals and GetHashCode ommitted } - private static SKTypeface CreateDefaultTypeface() + private static string CreateDefaultFamilyName() { - var defaultTypeface = SKTypeface.FromFamilyName(FontFamily.Default.Name) ?? SKTypeface.FromFamilyName(null); + var defaultTypeface = SKTypeface.CreateDefault(); - return defaultTypeface; + return defaultTypeface.FamilyName; } private static SKTypeface GetTypeface(string name, FontKey key) { var familyKey = name; - if (!Cache.TryGetValue(familyKey, out var entry)) + if (!s_cache.TryGetValue(familyKey, out var entry)) { - Cache[familyKey] = entry = new Dictionary(); + s_cache[familyKey] = entry = new Dictionary(); } if (!entry.TryGetValue(key, out var typeface)) { - typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant) - ?? Default; + typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant) ?? + GetTypeface(DefaultFamilyName, key); entry[key] = typeface; } @@ -78,7 +80,7 @@ namespace Avalonia.Skia public static SKTypeface GetTypeface(string name, FontStyle style, FontWeight weight) { - SKFontStyleSlant skStyle = SKFontStyleSlant.Upright; + var skStyle = SKFontStyleSlant.Upright; switch (style) { @@ -93,6 +95,5 @@ namespace Avalonia.Skia return GetTypeface(name, new FontKey((SKFontStyleWeight)weight, skStyle)); } - } } diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index 70b2703f5e..16f2767096 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -1,6 +1,7 @@  netstandard2.0 + Avalonia.Direct2D1 diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfMouseDevice.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfMouseDevice.cs index 0d93115714..ebfe8cde47 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfMouseDevice.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfMouseDevice.cs @@ -9,22 +9,32 @@ namespace Avalonia.Win32.Interop.Wpf { private readonly WpfTopLevelImpl _impl; - public WpfMouseDevice(WpfTopLevelImpl impl) + public WpfMouseDevice(WpfTopLevelImpl impl) : base(new WpfMousePointer(impl)) { _impl = impl; } - public override void Capture(IInputElement control) + class WpfMousePointer : Pointer { - if (control == null) + private readonly WpfTopLevelImpl _impl; + + public WpfMousePointer(WpfTopLevelImpl impl) : base(Pointer.GetNextFreeId(), PointerType.Mouse, true) + { + _impl = impl; + } + + protected override void PlatformCapture(IInputElement control) { - System.Windows.Input.Mouse.Capture(null); + if (control == null) + { + System.Windows.Input.Mouse.Capture(null); + } + else if ((control.GetVisualRoot() as EmbeddableControlRoot)?.PlatformImpl != _impl) + throw new ArgumentException("Visual belongs to unknown toplevel"); + else + System.Windows.Input.Mouse.Capture(_impl); } - else if ((control.GetVisualRoot() as EmbeddableControlRoot)?.PlatformImpl != _impl) - throw new ArgumentException("Visual belongs to unknown toplevel"); - else - System.Windows.Input.Mouse.Capture(_impl); - base.Capture(control); } + } } diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 7005459487..c89d0a15cf 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -160,19 +160,19 @@ namespace Avalonia.Win32.Interop.Wpf return rv; } - void MouseEvent(RawMouseEventType type, MouseEventArgs e) - => _ttl.Input?.Invoke(new RawMouseEventArgs(_mouse, (uint)e.Timestamp, _inputRoot, type, + void MouseEvent(RawPointerEventType type, MouseEventArgs e) + => _ttl.Input?.Invoke(new RawPointerEventArgs(_mouse, (uint)e.Timestamp, _inputRoot, type, e.GetPosition(this).ToAvaloniaPoint(), GetModifiers())); protected override void OnMouseDown(MouseButtonEventArgs e) { - RawMouseEventType type; + RawPointerEventType type; if(e.ChangedButton == MouseButton.Left) - type = RawMouseEventType.LeftButtonDown; + type = RawPointerEventType.LeftButtonDown; else if (e.ChangedButton == MouseButton.Middle) - type = RawMouseEventType.MiddleButtonDown; + type = RawPointerEventType.MiddleButtonDown; else if (e.ChangedButton == MouseButton.Right) - type = RawMouseEventType.RightButtonDown; + type = RawPointerEventType.RightButtonDown; else return; MouseEvent(type, e); @@ -181,13 +181,13 @@ namespace Avalonia.Win32.Interop.Wpf protected override void OnMouseUp(MouseButtonEventArgs e) { - RawMouseEventType type; + RawPointerEventType type; if (e.ChangedButton == MouseButton.Left) - type = RawMouseEventType.LeftButtonUp; + type = RawPointerEventType.LeftButtonUp; else if (e.ChangedButton == MouseButton.Middle) - type = RawMouseEventType.MiddleButtonUp; + type = RawPointerEventType.MiddleButtonUp; else if (e.ChangedButton == MouseButton.Right) - type = RawMouseEventType.RightButtonUp; + type = RawPointerEventType.RightButtonUp; else return; MouseEvent(type, e); @@ -196,14 +196,14 @@ namespace Avalonia.Win32.Interop.Wpf protected override void OnMouseMove(MouseEventArgs e) { - MouseEvent(RawMouseEventType.Move, e); + MouseEvent(RawPointerEventType.Move, e); } protected override void OnMouseWheel(MouseWheelEventArgs e) => _ttl.Input?.Invoke(new RawMouseWheelEventArgs(_mouse, (uint) e.Timestamp, _inputRoot, e.GetPosition(this).ToAvaloniaPoint(), new Vector(0, e.Delta), GetModifiers())); - protected override void OnMouseLeave(MouseEventArgs e) => MouseEvent(RawMouseEventType.LeaveWindow, e); + protected override void OnMouseLeave(MouseEventArgs e) => MouseEvent(RawPointerEventType.LeaveWindow, e); protected override void OnKeyDown(KeyEventArgs e) => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint) e.Timestamp, RawKeyEventType.KeyDown, diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index d1046c6133..67db56dd1b 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -2,6 +2,7 @@ netstandard2.0 true + Avalonia.Win32 diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index e582b5fb82..f1fd74f931 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -41,6 +41,7 @@ namespace Avalonia.Win32 private static readonly Dictionary CursorTypeMapping = new Dictionary { + {StandardCursorType.None, 0}, {StandardCursorType.AppStarting, 32650}, {StandardCursorType.Arrow, 32512}, {StandardCursorType.Cross, 32515}, diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs index 2b4105efee..e7c379ad89 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs @@ -1,7 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using Avalonia.Controls; using Avalonia.Input; +using Avalonia.VisualTree; using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input @@ -10,23 +13,32 @@ namespace Avalonia.Win32.Input { public static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice(); + public WindowsMouseDevice() : base(new WindowsMousePointer()) + { + + } + public WindowImpl CurrentWindow { get; set; } - public override void Capture(IInputElement control) + class WindowsMousePointer : Pointer { - base.Capture(control); - - if (control != null) + public WindowsMousePointer() : base(Pointer.GetNextFreeId(),PointerType.Mouse, true) { - UnmanagedMethods.SetCapture(CurrentWindow.Handle.Handle); } - else + + protected override void PlatformCapture(IInputElement element) { - UnmanagedMethods.ReleaseCapture(); + var hwnd = ((element?.GetVisualRoot() as TopLevel)?.PlatformImpl as WindowImpl) + ?.Handle.Handle; + + if (hwnd.HasValue && hwnd != IntPtr.Zero) + UnmanagedMethods.SetCapture(hwnd.Value); + else + UnmanagedMethods.ReleaseCapture(); } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 7d6e8fc8ce..bc7fc1c9fa 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -574,6 +574,7 @@ namespace Avalonia.Win32.Interop WM_AFXLAST = 0x037F, WM_PENWINFIRST = 0x0380, WM_PENWINLAST = 0x038F, + WM_TOUCH = 0x0240, WM_APP = 0x8000, WM_USER = 0x0400, @@ -836,10 +837,16 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); + + [DllImport("user32")] + public static extern IntPtr GetMessageExtraInfo(); [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "RegisterClassExW")] public static extern ushort RegisterClassEx(ref WNDCLASSEX lpwcx); + [DllImport("user32.dll")] + public static extern void RegisterTouchWindow(IntPtr hWnd, int flags); + [DllImport("user32.dll")] public static extern bool ReleaseCapture(); @@ -1035,6 +1042,17 @@ namespace Avalonia.Win32.Interop [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetMonitorInfo([In] IntPtr hMonitor, ref MONITORINFO lpmi); + [DllImport("user32")] + public static extern bool GetTouchInputInfo( + IntPtr hTouchInput, + uint cInputs, + [Out]TOUCHINPUT[] pInputs, + int cbSize + ); + + [DllImport("user32")] + public static extern bool CloseTouchInputHandle(IntPtr hTouchInput); + [return: MarshalAs(UnmanagedType.Bool)] [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")] public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); @@ -1309,6 +1327,60 @@ namespace Avalonia.Win32.Interop public IntPtr hIconSm; } + [StructLayout(LayoutKind.Sequential)] + public struct TOUCHINPUT + { + public int X; + public int Y; + public IntPtr Source; + public uint Id; + public TouchInputFlags Flags; + public int Mask; + public uint Time; + public IntPtr ExtraInfo; + public int CxContact; + public int CyContact; + } + + [Flags] + public enum TouchInputFlags + { + /// + /// Movement has occurred. Cannot be combined with TOUCHEVENTF_DOWN. + /// + TOUCHEVENTF_MOVE = 0x0001, + + /// + /// The corresponding touch point was established through a new contact. Cannot be combined with TOUCHEVENTF_MOVE or TOUCHEVENTF_UP. + /// + TOUCHEVENTF_DOWN = 0x0002, + + /// + /// A touch point was removed. + /// + TOUCHEVENTF_UP = 0x0004, + + /// + /// A touch point is in range. This flag is used to enable touch hover support on compatible hardware. Applications that do not want support for hover can ignore this flag. + /// + TOUCHEVENTF_INRANGE = 0x0008, + + /// + /// Indicates that this TOUCHINPUT structure corresponds to a primary contact point. See the following text for more information on primary touch points. + /// + TOUCHEVENTF_PRIMARY = 0x0010, + + /// + /// When received using GetTouchInputInfo, this input was not coalesced. + /// + TOUCHEVENTF_NOCOALESCE = 0x0020, + + /// + /// The touch event came from the user's palm. + /// + TOUCHEVENTF_PALM = 0x0080 + } + [Flags] public enum OpenFileNameFlags { diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index f679c2410e..c45bf6389e 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -40,6 +40,7 @@ namespace Avalonia { public bool UseDeferredRendering { get; set; } = true; public bool AllowEglInitialization { get; set; } + public bool? EnableMultitouch { get; set; } } } @@ -59,7 +60,8 @@ namespace Avalonia.Win32 CreateMessageWindow(); } - public static bool UseDeferredRendering { get; set; } + public static bool UseDeferredRendering => Options.UseDeferredRendering; + public static Win32PlatformOptions Options { get; private set; } public Size DoubleClickSize => new Size( UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK), @@ -74,6 +76,7 @@ namespace Avalonia.Win32 public static void Initialize(Win32PlatformOptions options) { + Options = options; AvaloniaLocator.CurrentMutable .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) @@ -88,7 +91,7 @@ namespace Avalonia.Win32 .Bind().ToConstant(s_instance); if (options.AllowEglInitialization) Win32GlManager.Initialize(); - UseDeferredRendering = options.UseDeferredRendering; + _uiThread = Thread.CurrentThread; if (OleContext.Current != null) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 081a713e95..5cc148fa0d 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -30,7 +30,10 @@ namespace Avalonia.Win32 private UnmanagedMethods.WndProc _wndProcDelegate; private string _className; private IntPtr _hwnd; + private bool _multitouch; + private TouchDevice _touchDevice = new TouchDevice(); private IInputRoot _owner; + private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock(); private bool _trackingMouse; private bool _decorated = true; private bool _resizable = true; @@ -148,7 +151,9 @@ namespace Avalonia.Win32 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); - return Win32Platform.UseDeferredRendering ? (IRenderer)new DeferredRenderer(root, loop) : new ImmediateRenderer(root); + return Win32Platform.UseDeferredRendering ? + (IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock) : + new ImmediateRenderer(root); } public void Resize(Size value) @@ -331,6 +336,7 @@ namespace Avalonia.Win32 public void BeginMoveDrag() { + WindowsMouseDevice.Instance.Capture(null); UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN, new IntPtr((int)UnmanagedMethods.HitTestValues.HTCAPTION), IntPtr.Zero); } @@ -352,6 +358,7 @@ namespace Avalonia.Win32 #if USE_MANAGED_DRAG _managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position)); #else + WindowsMouseDevice.Instance.Capture(null); UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN, new IntPtr((int)EdgeDic[edge]), IntPtr.Zero); #endif @@ -414,6 +421,15 @@ namespace Avalonia.Win32 IntPtr.Zero); } + bool ShouldIgnoreTouchEmulatedMessage() + { + if (!_multitouch) + return false; + var marker = 0xFF515700L; + var info = GetMessageExtraInfo().ToInt64(); + return (info & marker) == marker; + } + [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")] protected virtual IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { @@ -519,34 +535,40 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN: - e = new RawMouseEventArgs( + if(ShouldIgnoreTouchEmulatedMessage()) + break; + e = new RawPointerEventArgs( WindowsMouseDevice.Instance, timestamp, _owner, msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN - ? RawMouseEventType.LeftButtonDown + ? RawPointerEventType.LeftButtonDown : msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN - ? RawMouseEventType.RightButtonDown - : RawMouseEventType.MiddleButtonDown, + ? RawPointerEventType.RightButtonDown + : RawPointerEventType.MiddleButtonDown, DipFromLParam(lParam), GetMouseModifiers(wParam)); break; case UnmanagedMethods.WindowsMessage.WM_LBUTTONUP: case UnmanagedMethods.WindowsMessage.WM_RBUTTONUP: case UnmanagedMethods.WindowsMessage.WM_MBUTTONUP: - e = new RawMouseEventArgs( + if(ShouldIgnoreTouchEmulatedMessage()) + break; + e = new RawPointerEventArgs( WindowsMouseDevice.Instance, timestamp, _owner, msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP - ? RawMouseEventType.LeftButtonUp + ? RawPointerEventType.LeftButtonUp : msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONUP - ? RawMouseEventType.RightButtonUp - : RawMouseEventType.MiddleButtonUp, + ? RawPointerEventType.RightButtonUp + : RawPointerEventType.MiddleButtonUp, DipFromLParam(lParam), GetMouseModifiers(wParam)); break; case UnmanagedMethods.WindowsMessage.WM_MOUSEMOVE: + if(ShouldIgnoreTouchEmulatedMessage()) + break; if (!_trackingMouse) { var tm = new UnmanagedMethods.TRACKMOUSEEVENT @@ -560,11 +582,11 @@ namespace Avalonia.Win32 UnmanagedMethods.TrackMouseEvent(ref tm); } - e = new RawMouseEventArgs( + e = new RawPointerEventArgs( WindowsMouseDevice.Instance, timestamp, _owner, - RawMouseEventType.Move, + RawPointerEventType.Move, DipFromLParam(lParam), GetMouseModifiers(wParam)); break; @@ -589,29 +611,50 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_MOUSELEAVE: _trackingMouse = false; - e = new RawMouseEventArgs( + e = new RawPointerEventArgs( WindowsMouseDevice.Instance, timestamp, _owner, - RawMouseEventType.LeaveWindow, + RawPointerEventType.LeaveWindow, new Point(), WindowsKeyboardDevice.Instance.Modifiers); break; case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN: - e = new RawMouseEventArgs( + e = new RawPointerEventArgs( WindowsMouseDevice.Instance, timestamp, _owner, msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN - ? RawMouseEventType.NonClientLeftButtonDown + ? RawPointerEventType.NonClientLeftButtonDown : msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN - ? RawMouseEventType.RightButtonDown - : RawMouseEventType.MiddleButtonDown, + ? RawPointerEventType.RightButtonDown + : RawPointerEventType.MiddleButtonDown, new Point(0, 0), GetMouseModifiers(wParam)); break; - + case WindowsMessage.WM_TOUCH: + var touchInputs = new TOUCHINPUT[wParam.ToInt32()]; + if (GetTouchInputInfo(lParam, (uint)wParam.ToInt32(), touchInputs, Marshal.SizeOf())) + { + foreach (var touchInput in touchInputs) + { + Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, + _owner, + touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ? + RawPointerEventType.TouchEnd : + touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ? + RawPointerEventType.TouchBegin : + RawPointerEventType.TouchUpdate, + PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)), + WindowsKeyboardDevice.Instance.Modifiers, + touchInput.Id)); + } + CloseTouchInputHandle(lParam); + return IntPtr.Zero; + } + + break; case WindowsMessage.WM_NCPAINT: if (!_decorated) { @@ -627,18 +670,26 @@ namespace Avalonia.Win32 break; case UnmanagedMethods.WindowsMessage.WM_PAINT: - UnmanagedMethods.PAINTSTRUCT ps; - if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) + using (_rendererLock.Lock()) { - var f = Scaling; - var r = ps.rcPaint; - Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, (r.bottom - r.top) / f)); - UnmanagedMethods.EndPaint(_hwnd, ref ps); + UnmanagedMethods.PAINTSTRUCT ps; + if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) + { + var f = Scaling; + var r = ps.rcPaint; + Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, + (r.bottom - r.top) / f)); + UnmanagedMethods.EndPaint(_hwnd, ref ps); + } } return IntPtr.Zero; case UnmanagedMethods.WindowsMessage.WM_SIZE: + using (_rendererLock.Lock()) + { + // Do nothing here, just block until the pending frame render is completed on the render thread + } var size = (UnmanagedMethods.SizeCommand)wParam; if (Resized != null && @@ -704,7 +755,8 @@ namespace Avalonia.Win32 } } - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + using (_rendererLock.Lock()) + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); } static InputModifiers GetMouseModifiers(IntPtr wParam) @@ -754,6 +806,10 @@ namespace Avalonia.Win32 Handle = new PlatformHandle(_hwnd, PlatformConstants.WindowHandleType); + _multitouch = Win32Platform.Options.EnableMultitouch ?? false; + if (_multitouch) + RegisterTouchWindow(_hwnd, 0); + if (UnmanagedMethods.ShCoreAvailable) { uint dpix, dpiy; diff --git a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj index 683c256b7b..e57fcc643f 100644 --- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj +++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj @@ -6,6 +6,7 @@ + diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index 83d10b8b44..15e8b35056 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -86,11 +86,11 @@ namespace Avalonia.iOS { var location = touch.LocationInView(this).ToAvalonia(); - Input?.Invoke(new RawMouseEventArgs( + Input?.Invoke(new RawPointerEventArgs( iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, - RawMouseEventType.LeftButtonUp, + RawPointerEventType.LeftButtonUp, location, InputModifiers.None)); } @@ -104,11 +104,11 @@ namespace Avalonia.iOS { var location = touch.LocationInView(this).ToAvalonia(); _touchLastPoint = location; - Input?.Invoke(new RawMouseEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, - RawMouseEventType.Move, location, InputModifiers.None)); + Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, + RawPointerEventType.Move, location, InputModifiers.None)); - Input?.Invoke(new RawMouseEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, - RawMouseEventType.LeftButtonDown, location, InputModifiers.None)); + Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, + RawPointerEventType.LeftButtonDown, location, InputModifiers.None)); } } @@ -119,8 +119,8 @@ namespace Avalonia.iOS { var location = touch.LocationInView(this).ToAvalonia(); if (iOSPlatform.MouseDevice.Captured != null) - Input?.Invoke(new RawMouseEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, - RawMouseEventType.Move, location, InputModifiers.LeftMouseButton)); + Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot, + RawPointerEventType.Move, location, InputModifiers.LeftMouseButton)); else { //magic number based on test - correction of 0.02 is working perfect diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 9a751d4953..994804e9e1 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -14,6 +14,8 @@ namespace Avalonia.Controls.UnitTests { public class ButtonTests { + private MouseTestHelper _helper = new MouseTestHelper(); + [Fact] public void Button_Is_Disabled_When_Command_Is_Disabled() { @@ -24,11 +26,26 @@ namespace Avalonia.Controls.UnitTests }; var root = new TestRoot { Child = target }; - Assert.False(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); command.IsEnabled = true; - Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); command.IsEnabled = false; - Assert.False(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + } + + [Fact] + public void Button_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False() + { + var command = new TestCommand(true); + var target = new Button + { + IsEnabled = false, + Command = command, + }; + + var root = new TestRoot { Child = target }; + + Assert.False(((IInputElement)target).IsEffectivelyEnabled); } [Fact] @@ -39,7 +56,8 @@ namespace Avalonia.Controls.UnitTests [!Button.CommandProperty] = new Binding("Command"), }; - Assert.False(target.IsEnabled); + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); } [Fact] @@ -57,8 +75,12 @@ namespace Avalonia.Controls.UnitTests }; Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + target.DataContext = null; - Assert.False(target.IsEnabled); + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); } [Fact] @@ -75,9 +97,13 @@ namespace Avalonia.Controls.UnitTests [!Button.CommandProperty] = new Binding("Command"), }; - Assert.False(target.IsEnabled); + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + target.DataContext = viewModel; + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); } [Fact] @@ -94,20 +120,20 @@ namespace Avalonia.Controls.UnitTests [!Button.CommandProperty] = new Binding("Command"), }; - Assert.False(target.IsEnabled); + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + target.DataContext = viewModel; - Assert.False(target.IsEnabled); + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); } [Fact] public void Button_Raises_Click() { - var mouse = Mock.Of(); var renderer = Mock.Of(); - IInputElement captured = null; - Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(50, 50)); - Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); - Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + var pt = new Point(50, 50); Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]); @@ -122,15 +148,15 @@ namespace Avalonia.Controls.UnitTests target.Click += (s, e) => clicked = true; - RaisePointerEnter(target, mouse); - RaisePointerMove(target, mouse); - RaisePointerPressed(target, mouse, 1, MouseButton.Left); + RaisePointerEnter(target); + RaisePointerMove(target, pt); + RaisePointerPressed(target, 1, MouseButton.Left, pt); - Assert.Equal(captured, target); + Assert.Equal(_helper.Captured, target); - RaisePointerReleased(target, mouse, MouseButton.Left); + RaisePointerReleased(target, MouseButton.Left, pt); - Assert.Equal(captured, null); + Assert.Equal(_helper.Captured, null); Assert.True(clicked); } @@ -138,12 +164,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside() { - var mouse = Mock.Of(); var renderer = Mock.Of(); - IInputElement captured = null; - Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(200, 50)); - Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); - Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]); @@ -158,16 +180,16 @@ namespace Avalonia.Controls.UnitTests target.Click += (s, e) => clicked = true; - RaisePointerEnter(target, mouse); - RaisePointerMove(target, mouse); - RaisePointerPressed(target, mouse, 1, MouseButton.Left); - RaisePointerLeave(target, mouse); + RaisePointerEnter(target); + RaisePointerMove(target, new Point(50,50)); + RaisePointerPressed(target, 1, MouseButton.Left, new Point(50, 50)); + RaisePointerLeave(target); - Assert.Equal(captured, target); + Assert.Equal(_helper.Captured, target); - RaisePointerReleased(target, mouse, MouseButton.Left); + RaisePointerReleased(target, MouseButton.Left, new Point(200, 50)); - Assert.Equal(captured, null); + Assert.Equal(_helper.Captured, null); Assert.False(clicked); } @@ -175,12 +197,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Button_With_RenderTransform_Raises_Click() { - var mouse = Mock.Of(); var renderer = Mock.Of(); - IInputElement captured = null; - Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(150, 50)); - Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); - Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + var pt = new Point(150, 50); Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ? @@ -204,15 +222,15 @@ namespace Avalonia.Controls.UnitTests target.Click += (s, e) => clicked = true; - RaisePointerEnter(target, mouse); - RaisePointerMove(target, mouse); - RaisePointerPressed(target, mouse, 1, MouseButton.Left); + RaisePointerEnter(target); + RaisePointerMove(target, pt); + RaisePointerPressed(target, 1, MouseButton.Left, pt); - Assert.Equal(captured, target); + Assert.Equal(_helper.Captured, target); - RaisePointerReleased(target, mouse, MouseButton.Left); + RaisePointerReleased(target, MouseButton.Left, pt); - Assert.Equal(captured, null); + Assert.Equal(_helper.Captured, null); Assert.True(clicked); } @@ -278,57 +296,29 @@ namespace Avalonia.Controls.UnitTests public PixelPoint PointToScreen(Point p) => throw new NotImplementedException(); } - private void RaisePointerPressed(Button button, IMouseDevice device, int clickCount, MouseButton mouseButton) + private void RaisePointerPressed(Button button, int clickCount, MouseButton mouseButton, Point position) { - button.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - Source = button, - MouseButton = mouseButton, - ClickCount = clickCount, - Device = device, - }); + _helper.Down(button, mouseButton, position, clickCount: clickCount); } - private void RaisePointerReleased(Button button, IMouseDevice device, MouseButton mouseButton) + private void RaisePointerReleased(Button button, MouseButton mouseButton, Point pt) { - button.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - Source = button, - MouseButton = mouseButton, - Device = device, - }); + _helper.Up(button, mouseButton, pt); } - private void RaisePointerEnter(Button button, IMouseDevice device) + private void RaisePointerEnter(Button button) { - button.RaiseEvent(new PointerEventArgs - { - RoutedEvent = InputElement.PointerEnterEvent, - Source = button, - Device = device, - }); + _helper.Enter(button); } - private void RaisePointerLeave(Button button, IMouseDevice device) + private void RaisePointerLeave(Button button) { - button.RaiseEvent(new PointerEventArgs - { - RoutedEvent = InputElement.PointerLeaveEvent, - Source = button, - Device = device, - }); + _helper.Leave(button); } - private void RaisePointerMove(Button button, IMouseDevice device) + private void RaisePointerMove(Button button, Point pos) { - button.RaiseEvent(new PointerEventArgs - { - RoutedEvent = InputElement.PointerMovedEvent, - Source = button, - Device = device, - }); + _helper.Move(button, pos); } private class TestCommand : ICommand diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index fdbca70350..70ec6c1408 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -15,6 +15,8 @@ namespace Avalonia.Controls.UnitTests { public class ComboBoxTests { + MouseTestHelper _helper = new MouseTestHelper(); + [Fact] public void Clicking_On_Control_Toggles_IsDropDownOpen() { @@ -23,17 +25,11 @@ namespace Avalonia.Controls.UnitTests Items = new[] { "Foo", "Bar" }, }; - target.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - }); - + _helper.Down(target); + _helper.Up(target); Assert.True(target.IsDropDownOpen); - target.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - }); + _helper.Down(target); Assert.False(target.IsDropDownOpen); } diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 834c49ba6b..067c66969f 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -14,6 +14,7 @@ namespace Avalonia.Controls.UnitTests public class ContextMenuTests { private Mock popupImpl; + private MouseTestHelper _mouse = new MouseTestHelper(); [Fact] public void Clicking_On_Control_Toggles_ContextMenu() @@ -31,19 +32,11 @@ namespace Avalonia.Controls.UnitTests new Window { Content = target }; - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.None - }); + _mouse.Click(target); Assert.False(sut.IsOpen); popupImpl.Verify(x => x.Show(), Times.Once); @@ -69,19 +62,11 @@ namespace Avalonia.Controls.UnitTests Avalonia.Application.Current.MainWindow = window; - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); popupImpl.Verify(x => x.Hide(), Times.Once); @@ -106,11 +91,7 @@ namespace Avalonia.Controls.UnitTests sut.ContextMenuOpening += (c, e) => { eventCalled = true; e.Cancel = true; }; - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(eventCalled); Assert.False(sut.IsOpen); @@ -136,19 +117,11 @@ namespace Avalonia.Controls.UnitTests sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; }; - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.None - }); + _mouse.Click(target, MouseButton.Right); Assert.True(eventCalled); Assert.True(sut.IsOpen); diff --git a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs deleted file mode 100644 index 93163f4a92..0000000000 --- a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Avalonia.Controls.Utils; -using Xunit; - -namespace Avalonia.Controls.UnitTests -{ - public class GridLayoutTests - { - private const double Inf = double.PositiveInfinity; - - [Theory] - [InlineData("100, 200, 300", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("100, 200, 300", 800d, 600d, new[] { 100d, 200d, 300d })] - [InlineData("100, 200, 300", 600d, 600d, new[] { 100d, 200d, 300d })] - [InlineData("100, 200, 300", 400d, 400d, new[] { 100d, 200d, 100d })] - public void MeasureArrange_AllPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("*,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("*,2*,3*", 600d, 0d, new[] { 100d, 200d, 300d })] - public void MeasureArrange_AllStarLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("100,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("100,2*,3*", 600d, 100d, new[] { 100d, 200d, 300d })] - [InlineData("100,2*,3*", 100d, 100d, new[] { 100d, 0d, 0d })] - [InlineData("100,2*,3*", 50d, 50d, new[] { 50d, 0d, 0d })] - public void MeasureArrange_MixStarPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("100,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("100,200,Auto", 600d, 300d, new[] { 100d, 200d, 0d })] - [InlineData("100,200,Auto", 300d, 300d, new[] { 100d, 200d, 0d })] - [InlineData("100,200,Auto", 200d, 200d, new[] { 100d, 100d, 0d })] - [InlineData("100,200,Auto", 100d, 100d, new[] { 100d, 0d, 0d })] - [InlineData("100,200,Auto", 50d, 50d, new[] { 50d, 0d, 0d })] - public void MeasureArrange_MixAutoPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("*,2*,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("*,2*,Auto", 600d, 0d, new[] { 200d, 400d, 0d })] - public void MeasureArrange_MixAutoStarLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("*,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("*,200,Auto", 600d, 200d, new[] { 400d, 200d, 0d })] - [InlineData("*,200,Auto", 200d, 200d, new[] { 0d, 200d, 0d })] - [InlineData("*,200,Auto", 100d, 100d, new[] { 0d, 100d, 0d })] - public void MeasureArrange_MixAutoStarPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - - /// - /// This is needed because Mono somehow converts double array to object array in attribute metadata - /// - static void AssertEqual(IList expected, IReadOnlyList actual) - { - var conv = expected.Cast().ToArray(); - Assert.Equal(conv, actual); - } - - [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local")] - private static void TestRowDefinitionsOnly(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - // Arrange - var layout = new GridLayout(new RowDefinitions(length)); - - // Measure - Action & Assert - var measure = layout.Measure(containerLength); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedLengthList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange(containerLength, measure); - AssertEqual(expectedLengthList, arrange.LengthList); - } - - [Theory] - [InlineData("100, 200, 300", 600d, new[] { 100d, 200d, 300d }, new[] { 100d, 200d, 300d })] - [InlineData("*,2*,3*", 0d, new[] { Inf, Inf, Inf }, new[] { 0d, 0d, 0d })] - [InlineData("100,2*,3*", 100d, new[] { 100d, Inf, Inf }, new[] { 100d, 0d, 0d })] - [InlineData("100,200,Auto", 300d, new[] { 100d, 200d, 0d }, new[] { 100d, 200d, 0d })] - [InlineData("*,2*,Auto", 0d, new[] { Inf, Inf, 0d }, new[] { 0d, 0d, 0d })] - [InlineData("*,200,Auto", 200d, new[] { Inf, 200d, 0d }, new[] { 0d, 200d, 0d })] - public void MeasureArrange_InfiniteMeasure_Correct(string length, double expectedDesiredLength, - IList expectedMeasureList, IList expectedArrangeList) - { - // Arrange - var layout = new GridLayout(new RowDefinitions(length)); - - // Measure - Action & Assert - var measure = layout.Measure(Inf); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedMeasureList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange(measure.DesiredLength, measure); - AssertEqual(expectedArrangeList, arrange.LengthList); - } - - [Theory] - [InlineData("Auto,*,*", new[] { 100d, 100d, 100d }, 600d, 300d, new[] { 100d, 250d, 250d })] - public void MeasureArrange_ChildHasSize_Correct(string length, - IList childLengthList, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - // Arrange - var lengthList = new ColumnDefinitions(length); - var layout = new GridLayout(lengthList); - layout.AppendMeasureConventions( - Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, 1)), - x => (double)childLengthList[x]); - - // Measure - Action & Assert - var measure = layout.Measure(containerLength); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedLengthList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange(containerLength, measure); - AssertEqual(expectedLengthList, arrange.LengthList); - } - - [Theory] - [InlineData(Inf, 250d, new[] { 100d, Inf, Inf }, new[] { 100d, 50d, 100d })] - [InlineData(400d, 250d, new[] { 100d, 100d, 200d }, new[] { 100d, 100d, 200d })] - [InlineData(325d, 250d, new[] { 100d, 75d, 150d }, new[] { 100d, 75d, 150d })] - [InlineData(250d, 250d, new[] { 100d, 50d, 100d }, new[] { 100d, 50d, 100d })] - [InlineData(160d, 160d, new[] { 100d, 20d, 40d }, new[] { 100d, 20d, 40d })] - public void MeasureArrange_ChildHasSizeAndHasMultiSpan_Correct( - double containerLength, double expectedDesiredLength, - IList expectedMeasureLengthList, IList expectedArrangeLengthList) - { - var length = "100,*,2*"; - var childLengthList = new[] { 150d, 150d, 150d }; - var spans = new[] { 1, 2, 1 }; - - // Arrange - var lengthList = new ColumnDefinitions(length); - var layout = new GridLayout(lengthList); - layout.AppendMeasureConventions( - Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, spans[x])), - x => childLengthList[x]); - - // Measure - Action & Assert - var measure = layout.Measure(containerLength); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedMeasureLengthList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange( - double.IsInfinity(containerLength) ? measure.DesiredLength : containerLength, - measure); - AssertEqual(expectedArrangeLengthList, arrange.LengthList); - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 5799cb91c4..df804d5d8c 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1,12 +1,73 @@ -// 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.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Moq; using Xunit; +using Xunit.Abstractions; namespace Avalonia.Controls.UnitTests { public class GridTests { + private readonly ITestOutputHelper output; + + public GridTests(ITestOutputHelper output) + { + this.output = output; + } + + private Grid CreateGrid(params (string name, GridLength width)[] columns) + { + return CreateGrid(columns.Select(c => + (c.name, c.width, ColumnDefinition.MinWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); + } + + private Grid CreateGrid(params (string name, GridLength width, double minWidth)[] columns) + { + return CreateGrid(columns.Select(c => + (c.name, c.width, c.minWidth, ColumnDefinition.MaxWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); + } + + private Grid CreateGrid(params (string name, GridLength width, double minWidth, double maxWidth)[] columns) + { + + var grid = new Grid(); + foreach (var k in columns.Select(c => new ColumnDefinition + { + SharedSizeGroup = c.name, + Width = c.width, + MinWidth = c.minWidth, + MaxWidth = c.maxWidth + })) + grid.ColumnDefinitions.Add(k); + + return grid; + } + + private Control AddSizer(Grid grid, int column, double size = 30) + { + var ctrl = new Control { MinWidth = size, MinHeight = size }; + ctrl.SetValue(Grid.ColumnProperty, column); + grid.Children.Add(ctrl); + output.WriteLine($"[AddSizer] Column: {column} MinWidth: {size} MinHeight: {size}"); + return ctrl; + } + + private void PrintColumnDefinitions(Grid grid) + { + output.WriteLine($"[Grid] ActualWidth: {grid.Bounds.Width} ActualHeight: {grid.Bounds.Width}"); + output.WriteLine($"[ColumnDefinitions]"); + for (int i = 0; i < grid.ColumnDefinitions.Count; i++) + { + var cd = grid.ColumnDefinitions[i]; + output.WriteLine($"[{i}] ActualWidth: {cd.ActualWidth} SharedSizeGroup: {cd.SharedSizeGroup}"); + } + } + [Fact] public void Calculates_Colspan_Correctly() { @@ -180,5 +241,1121 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.IsMeasureValid); } + [Fact] + public void Grid_GridLength_Same_Size_Pixel_0() + { + var grid = CreateGrid( + (null, new GridLength()), + (null, new GridLength()), + (null, new GridLength()), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, false); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void Grid_GridLength_Same_Size_Pixel_50() + { + var grid = CreateGrid( + (null, new GridLength(50)), + (null, new GridLength(50)), + (null, new GridLength(50)), + (null, new GridLength(50))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, false); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void Grid_GridLength_Same_Size_Auto() + { + var grid = CreateGrid( + (null, new GridLength(0, GridUnitType.Auto)), + (null, new GridLength(0, GridUnitType.Auto)), + (null, new GridLength(0, GridUnitType.Auto)), + (null, new GridLength(0, GridUnitType.Auto))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, false); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void Grid_GridLength_Same_Size_Star() + { + var grid = CreateGrid( + (null, new GridLength(1, GridUnitType.Star)), + (null, new GridLength(1, GridUnitType.Star)), + (null, new GridLength(1, GridUnitType.Star)), + (null, new GridLength(1, GridUnitType.Star))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, false); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0() + { + var grid = CreateGrid( + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50() + { + var grid = CreateGrid( + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto() + { + var grid = CreateGrid( + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star() + { + var grid = CreateGrid( + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star))); // Star sizing is treated as Auto, 1 is ignored + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_First_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_First_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_First_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_First_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star))); // Star sizing is treated as Auto, 1 is ignored + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_Last_Column_0() + { + var grid = CreateGrid( + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_Last_Column_0() + { + var grid = CreateGrid( + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_Last_Column_0() + { + var grid = CreateGrid( + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_Last_Column_0() + { + var grid = CreateGrid( + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_First_And_Last_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_First_And_Last_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_First_And_Last_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_First_And_Last_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength()), + ("B", new GridLength()), + ("B", new GridLength()), + ("A", new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength(25)), + ("B", new GridLength(75)), + ("B", new GridLength(75)), + ("A", new GridLength(25))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(25, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(75, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star))); // Star sizing is treated as Auto, 1 is ignored + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_First_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength()), + ("B", new GridLength()), + ("B", new GridLength()), + ("A", new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_First_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(25)), + ("B", new GridLength(75)), + ("B", new GridLength(75)), + ("A", new GridLength(25))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(25, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(75, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_First_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_First_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star))); // Star sizing is treated as Auto, 1 is ignored + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength()), + ("B", new GridLength()), + ("B", new GridLength()), + ("A", new GridLength()), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength(25)), + ("B", new GridLength(75)), + ("B", new GridLength(75)), + ("A", new GridLength(25)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(25, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(75, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_First_And_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength()), + ("B", new GridLength()), + ("B", new GridLength()), + ("A", new GridLength()), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_First_And_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(25)), + ("B", new GridLength(75)), + ("B", new GridLength(75)), + ("A", new GridLength(25)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(25, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(75, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_First_And_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_First_And_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void Size_Propagation_Is_Constrained_To_Innermost_Scope() + { + var grids = new[] { CreateGrid(("A", new GridLength())), CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; + var innerScope = new Grid(); + + foreach (var grid in grids) + innerScope.Children.Add(grid); + + innerScope.SetValue(Grid.IsSharedSizeScopeProperty, true); + + var outerGrid = CreateGrid(("A", new GridLength(0))); + var outerScope = new Grid(); + outerScope.Children.Add(outerGrid); + outerScope.Children.Add(innerScope); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(outerScope); + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + Assert.Equal(0, outerGrid.ColumnDefinitions[0].ActualWidth); + } + + [Fact] + public void Size_Group_Changes_Are_Tracked() + { + var grids = new[] { + CreateGrid((null, new GridLength(0, GridUnitType.Auto)), (null, new GridLength())), + CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; + var scope = new Grid(); + foreach (var xgrids in grids) + scope.Children.Add(xgrids); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + PrintColumnDefinitions(grids[0]); + Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); + + grids[0].ColumnDefinitions[0].SharedSizeGroup = "A"; + + root.Measure(new Size(51, 51)); + root.Arrange(new Rect(new Point(), new Point(51, 51))); + PrintColumnDefinitions(grids[0]); + Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); + + grids[0].ColumnDefinitions[0].SharedSizeGroup = null; + + root.Measure(new Size(52, 52)); + root.Arrange(new Rect(new Point(), new Point(52, 52))); + PrintColumnDefinitions(grids[0]); + Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); + } + + [Fact] + public void Collection_Changes_Are_Tracked() + { + var grid = CreateGrid( + ("A", new GridLength(20)), + ("A", new GridLength(30)), + ("A", new GridLength(40)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(40, cd.ActualWidth)); + + grid.ColumnDefinitions.RemoveAt(2); + + // NOTE: THIS IS BROKEN IN WPF + // grid.Measure(new Size(200, 200)); + // grid.Arrange(new Rect(new Point(), new Point(200, 200))); + // PrintColumnDefinitions(grid); + // Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); + + grid.ColumnDefinitions.Insert(1, new ColumnDefinition { Width = new GridLength(30), SharedSizeGroup = "A" }); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); + + grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(10), SharedSizeGroup = "A" }; + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); + + grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(50), SharedSizeGroup = "A" }; + + // NOTE: THIS IS BROKEN IN WPF + // grid.Measure(new Size(200, 200)); + // grid.Arrange(new Rect(new Point(), new Point(200, 200))); + // PrintColumnDefinitions(grid); + // Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void Size_Priorities_Are_Maintained() + { + var sizers = new List(); + var grid = CreateGrid( + ("A", new GridLength(20)), + ("A", new GridLength(20, GridUnitType.Auto)), + ("A", new GridLength(1, GridUnitType.Star)), + ("A", new GridLength(1, GridUnitType.Star)), + (null, new GridLength())); + for (int i = 0; i < 3; i++) + sizers.Add(AddSizer(grid, i, 6 + i * 6)); + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(100, 100)); + grid.Arrange(new Rect(new Point(), new Point(100, 100))); + PrintColumnDefinitions(grid); + // all in group are equal to the first fixed column + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(20, cd.ActualWidth)); + + grid.ColumnDefinitions[0].SharedSizeGroup = null; + + grid.Measure(new Size(100, 100)); + grid.Arrange(new Rect(new Point(), new Point(100, 100))); + PrintColumnDefinitions(grid); + + // NOTE: THIS IS BROKEN IN WPF + // Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 2 * 6, cd.ActualWidth)); + // grid.ColumnDefinitions[1].SharedSizeGroup = null; + + // grid.Measure(new Size(100, 100)); + // grid.Arrange(new Rect(new Point(), new Point(100, 100))); + // PrintColumnDefinitions(grid); + + // NOTE: THIS IS BROKEN IN WPF + // all in group are equal to width (MinWidth) of the sizer in the second column + // Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 1 * 6, cd.ActualWidth)); + + // NOTE: THIS IS BROKEN IN WPF + // grid.ColumnDefinitions[2].SharedSizeGroup = null; + + // NOTE: THIS IS BROKEN IN WPF + // grid.Measure(new Size(double.PositiveInfinity, 100)); + // grid.Arrange(new Rect(new Point(), new Point(100, 100))); + // PrintColumnDefinitions(grid); + // with no constraint star columns default to the MinWidth of the sizer in the column + // Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void ColumnDefinitions_Collection_Is_ReadOnly() + { + var grid = CreateGrid( + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); + + grid.ColumnDefinitions[0] = new ColumnDefinition { Width = new GridLength(25), SharedSizeGroup = "A" }; + grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(75), SharedSizeGroup = "B" }; + grid.ColumnDefinitions[2] = new ColumnDefinition { Width = new GridLength(75), SharedSizeGroup = "B" }; + grid.ColumnDefinitions[3] = new ColumnDefinition { Width = new GridLength(25), SharedSizeGroup = "A" }; + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(25, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(75, cd.ActualWidth)); + } + + [Fact] + public void ColumnDefinitions_Collection_Reset_SharedSizeGroup() + { + var grid = CreateGrid( + ("A", new GridLength(25)), + ("B", new GridLength(75)), + ("B", new GridLength(75)), + ("A", new GridLength(25))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(25, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(75, cd.ActualWidth)); + + grid.ColumnDefinitions[0].SharedSizeGroup = null; + grid.ColumnDefinitions[0].Width = new GridLength(50); + grid.ColumnDefinitions[1].SharedSizeGroup = null; + grid.ColumnDefinitions[1].Width = new GridLength(50); + grid.ColumnDefinitions[2].SharedSizeGroup = null; + grid.ColumnDefinitions[2].Width = new GridLength(50); + grid.ColumnDefinitions[3].SharedSizeGroup = null; + grid.ColumnDefinitions[3].Width = new GridLength(50); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(50, cd.ActualWidth)); + } } -} +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 343d8d41f3..238e214a5d 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -16,6 +16,8 @@ namespace Avalonia.Controls.UnitTests { public class ListBoxTests { + private MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] public void Should_Use_ItemTemplate_To_Create_Item_Content() { @@ -225,12 +227,7 @@ namespace Avalonia.Controls.UnitTests private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton) { - listBox.RaiseEvent(new PointerPressedEventArgs - { - Source = item, - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = mouseButton - }); + _mouse.Click(listBox, item, mouseButton); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index 70d59e82c8..de34558ad1 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -18,6 +18,8 @@ namespace Avalonia.Controls.UnitTests { public class ListBoxTests_Single { + MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] public void Focusing_Item_With_Tab_Should_Not_Select_It() { @@ -68,12 +70,7 @@ namespace Avalonia.Controls.UnitTests }; ApplyTemplate(target); - - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -90,11 +87,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); target.SelectedIndex = 0; - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -111,11 +104,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -133,11 +122,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); target.SelectedIndex = 0; - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(-1, target.SelectedIndex); } @@ -155,11 +140,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); target.SelectedIndex = 0; - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -177,11 +158,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); target.SelectedIndex = 1; - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -306,4 +283,4 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 32d154249c..34371916df 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Text; using System.Windows.Input; +using Avalonia.Data; +using Avalonia.Input; using Avalonia.UnitTests; using Xunit; @@ -25,6 +27,103 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.Focusable); } + + [Fact] + public void MenuItem_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False() + { + var command = new TestCommand(true); + var target = new MenuItem + { + IsEnabled = false, + Command = command, + }; + + var root = new TestRoot { Child = target }; + + Assert.False(((IInputElement)target).IsEffectivelyEnabled); + } + + [Fact] + public void MenuItem_Is_Disabled_When_Bound_Command_Doesnt_Exist() + { + var target = new MenuItem + { + [!MenuItem.CommandProperty] = new Binding("Command"), + }; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + } + + [Fact] + public void MenuItem_Is_Disabled_When_Bound_Command_Is_Removed() + { + var viewModel = new + { + Command = new TestCommand(true), + }; + + var target = new MenuItem + { + DataContext = viewModel, + [!MenuItem.CommandProperty] = new Binding("Command"), + }; + + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + + target.DataContext = null; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + } + + [Fact] + public void MenuItem_Is_Enabled_When_Bound_Command_Is_Added() + { + var viewModel = new + { + Command = new TestCommand(true), + }; + + var target = new MenuItem + { + DataContext = new object(), + [!MenuItem.CommandProperty] = new Binding("Command"), + }; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + + target.DataContext = viewModel; + + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + } + + [Fact] + public void MenuItem_Is_Disabled_When_Disabled_Bound_Command_Is_Added() + { + var viewModel = new + { + Command = new TestCommand(false), + }; + + var target = new MenuItem + { + DataContext = new object(), + [!MenuItem.CommandProperty] = new Binding("Command"), + }; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + + target.DataContext = viewModel; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + } + [Fact] public void MenuItem_Does_Not_Subscribe_To_Command_CanExecuteChanged_Until_Added_To_Logical_Tree() { @@ -60,8 +159,14 @@ namespace Avalonia.Controls.UnitTests private class TestCommand : ICommand { + private bool _enabled; private EventHandler _canExecuteChanged; + public TestCommand(bool enabled = true) + { + _enabled = enabled; + } + public int SubscriptionCount { get; private set; } public event EventHandler CanExecuteChanged @@ -70,7 +175,7 @@ namespace Avalonia.Controls.UnitTests remove { _canExecuteChanged -= value; --SubscriptionCount; } } - public bool CanExecute(object parameter) => true; + public bool CanExecute(object parameter) => _enabled; public void Execute(object parameter) { diff --git a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs new file mode 100644 index 0000000000..373bbaed75 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs @@ -0,0 +1,111 @@ +using System.Reactive; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.UnitTests +{ + public class MouseTestHelper + { + private Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + private ulong _nextStamp = 1; + private ulong Timestamp() => _nextStamp++; + + private InputModifiers _pressedButtons; + public IInputElement Captured => _pointer.Captured; + + InputModifiers Convert(MouseButton mouseButton) + => (mouseButton == MouseButton.Left ? InputModifiers.LeftMouseButton + : mouseButton == MouseButton.Middle ? InputModifiers.MiddleMouseButton + : mouseButton == MouseButton.Right ? InputModifiers.RightMouseButton : InputModifiers.None); + + int ButtonCount(PointerPointProperties props) + { + var rv = 0; + if (props.IsLeftButtonPressed) + rv++; + if (props.IsMiddleButtonPressed) + rv++; + if (props.IsRightButtonPressed) + rv++; + return rv; + } + + private MouseButton _pressedButton; + + InputModifiers GetModifiers(InputModifiers modifiers) => modifiers | _pressedButtons; + + public void Down(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default, + InputModifiers modifiers = default, int clickCount = 1) + { + Down(target, target, mouseButton, position, modifiers, clickCount); + } + + public void Down(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left, + Point position = default, InputModifiers modifiers = default, int clickCount = 1) + { + _pressedButtons |= Convert(mouseButton); + var props = new PointerPointProperties(_pressedButtons); + if (ButtonCount(props) > 1) + Move(target, source, position); + else + { + _pressedButton = mouseButton; + _pointer.Capture((IInputElement)target); + target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props, + GetModifiers(modifiers), clickCount)); + } + } + + public void Move(IInteractive target, in Point position, InputModifiers modifiers = default) => Move(target, target, position, modifiers); + public void Move(IInteractive target, IInteractive source, in Point position, InputModifiers modifiers = default) + { + target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (IVisual)target, position, + Timestamp(), new PointerPointProperties(_pressedButtons), GetModifiers(modifiers))); + } + + public void Up(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default, + InputModifiers modifiers = default) + => Up(target, target, mouseButton, position, modifiers); + + public void Up(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left, + Point position = default, InputModifiers modifiers = default) + { + var conv = Convert(mouseButton); + _pressedButtons = (_pressedButtons | conv) ^ conv; + var props = new PointerPointProperties(_pressedButtons); + if (ButtonCount(props) == 0) + { + _pointer.Capture(null); + target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, + Timestamp(), props, + GetModifiers(modifiers), _pressedButton)); + } + else + Move(target, source, position); + } + + public void Click(IInteractive target, MouseButton button = MouseButton.Left, Point position = default, + InputModifiers modifiers = default) + => Click(target, target, button, position, modifiers); + public void Click(IInteractive target, IInteractive source, MouseButton button = MouseButton.Left, + Point position = default, InputModifiers modifiers = default) + { + Down(target, source, button, position, modifiers); + Up(target, source, button, position, modifiers); + } + + public void Enter(IInteractive target) + { + target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnterEvent, target, _pointer, (IVisual)target, default, + Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons)); + } + + public void Leave(IInteractive target) + { + target.RaiseEvent(new PointerEventArgs(InputElement.PointerLeaveEvent, target, _pointer, (IVisual)target, default, + Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons)); + } + + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index df1846c617..ba4d6ca9c5 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; using Moq; using Xunit; @@ -9,6 +10,16 @@ namespace Avalonia.Controls.UnitTests.Platform { public class DefaultMenuInteractionHandlerTests { + static PointerEventArgs CreateArgs(RoutedEvent ev, IInteractive source) + => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, 0, new PointerPointProperties(), default); + + static PointerPressedEventArgs CreatePressed(IInteractive source) => new PointerPressedEventArgs(source, + new FakePointer(), (IVisual)source, default,0, new PointerPointProperties {IsLeftButtonPressed = true}, + default); + + static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source, + new FakePointer(), (IVisual)source, default,0, new PointerPointProperties(), default, MouseButton.Left); + public class TopLevel { [Fact] @@ -121,7 +132,8 @@ namespace Avalonia.Controls.UnitTests.Platform x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); - var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item }; + + var e = CreatePressed(item); target.PointerPressed(item, e); Mock.Get(menu).Verify(x => x.Close()); @@ -141,7 +153,7 @@ namespace Avalonia.Controls.UnitTests.Platform x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu.Object); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = nextItem }; + var e = CreateArgs(MenuItem.PointerEnterItemEvent, nextItem); menu.SetupGet(x => x.SelectedItem).Returns(item); @@ -161,7 +173,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); menu.SetupGet(x => x.SelectedItem).Returns(item); target.PointerLeave(item, e); @@ -176,7 +188,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); menu.SetupGet(x => x.IsOpen).Returns(true); menu.SetupGet(x => x.SelectedItem).Returns(item); @@ -330,7 +342,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerEnterItemEvent, item); target.PointerEnter(item, e); @@ -346,7 +358,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerEnterItemEvent, item); target.PointerEnter(item, e); Mock.Get(item).Verify(x => x.Open(), Times.Never); @@ -366,7 +378,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); var sibling = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsSubMenuOpen == true); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerEnterItemEvent, item); Mock.Get(parentItem).SetupGet(x => x.SubItems).Returns(new[] { item, sibling }); @@ -386,7 +398,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(item); target.PointerLeave(item, e); @@ -403,7 +415,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); var sibling = Mock.Of(x => x.Parent == parentItem); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(sibling); target.PointerLeave(item, e); @@ -419,7 +431,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsPointerOverSubMenu == true); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); target.PointerLeave(item, e); @@ -434,7 +446,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); - var e = new PointerReleasedEventArgs { MouseButton = MouseButton.Left, Source = item }; + var e = CreateReleased(item); target.PointerReleased(item, e); @@ -452,8 +464,8 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); var childItem = Mock.Of(x => x.Parent == item); - var enter = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; - var leave = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var enter = CreateArgs(MenuItem.PointerEnterItemEvent, item); + var leave = CreateArgs(MenuItem.PointerLeaveItemEvent, item); // Pointer enters item; item is selected. target.PointerEnter(item, enter); @@ -488,7 +500,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); - var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item }; + var e = CreatePressed(item); target.PointerPressed(item, e); @@ -537,5 +549,19 @@ namespace Avalonia.Controls.UnitTests.Platform _action = action; } } + + class FakePointer : IPointer + { + public int Id { get; } = Pointer.GetNextFreeId(); + + public void Capture(IInputElement control) + { + Captured = control; + } + + public IInputElement Captured { get; set; } + public PointerType Type { get; } + public bool IsPrimary { get; } = true; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 2df925301f..8e421bf0a2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -22,6 +22,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class SelectingItemsControlTests { + private MouseTestHelper _helper = new MouseTestHelper(); + [Fact] public void SelectedIndex_Should_Initially_Be_Minus_1() { @@ -341,6 +343,33 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(-1, target.SelectedIndex); } + [Fact] + public void Moving_Selected_Item_Should_Update_Selection() + { + var items = new AvaloniaList + { + new Item(), + new Item(), + }; + + var target = new SelectingItemsControl + { + Items = items, + Template = Template(), + }; + + target.ApplyTemplate(); + target.SelectedIndex = 0; + + Assert.Equal(items[0], target.SelectedItem); + Assert.Equal(0, target.SelectedIndex); + + items.Move(0, 1); + + Assert.Equal(items[1], target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + } + [Fact] public void Resetting_Items_Collection_Should_Clear_Selection() { @@ -648,12 +677,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - - target.Presenter.Panel.Children[1].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _helper.Down((Interactive)target.Presenter.Panel.Children[1]); var panel = target.Presenter.Panel; @@ -676,11 +700,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.Presenter.Panel.Children[1].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _helper.Down(target.Presenter.Panel.Children[1]); items.RemoveAt(1); @@ -729,6 +749,40 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal("b", target.SelectedItem); } + [Fact] + public void Mode_For_SelectedIndex_Is_TwoWay_By_Default() + { + var items = new[] + { + new Item(), + new Item(), + new Item(), + }; + + var vm = new MasterViewModel + { + Child = new ChildViewModel + { + Items = items, + SelectedIndex = 1, + } + }; + + var target = new SelectingItemsControl { DataContext = vm }; + var itemsBinding = new Binding("Child.Items"); + var selectedIndBinding = new Binding("Child.SelectedIndex"); + + target.Bind(SelectingItemsControl.ItemsProperty, itemsBinding); + target.Bind(SelectingItemsControl.SelectedIndexProperty, selectedIndBinding); + + Assert.Equal(1, target.SelectedIndex); + + target.SelectedIndex = 2; + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal(2, vm.Child.SelectedIndex); + } + private FuncControlTemplate Template() { return new FuncControlTemplate(control => @@ -765,6 +819,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { public IList Items { get; set; } public Item SelectedItem { get; set; } + public int SelectedIndex { get; set; } } private class RootWithItems : TestRoot diff --git a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs deleted file mode 100644 index 715e9da880..0000000000 --- a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls.Primitives; -using Avalonia.Input; -using Avalonia.Platform; -using Avalonia.UnitTests; - -using Moq; - -using Xunit; - -namespace Avalonia.Controls.UnitTests -{ - public class SharedSizeScopeTests - { - public SharedSizeScopeTests() - { - } - - [Fact] - public void All_Descendant_Grids_Are_Registered_When_Added_After_Setting_Scope() - { - var grids = new[] { new Grid(), new Grid(), new Grid() }; - var scope = new Panel(); - scope.Children.AddRange(grids); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; - - Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); - } - - [Fact] - public void All_Descendant_Grids_Are_Registered_When_Setting_Scope() - { - var grids = new[] { new Grid(), new Grid(), new Grid() }; - var scope = new Panel(); - scope.Children.AddRange(grids); - - var root = new TestRoot(); - root.Child = scope; - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - - Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); - } - - [Fact] - public void All_Descendant_Grids_Are_Unregistered_When_Resetting_Scope() - { - var grids = new[] { new Grid(), new Grid(), new Grid() }; - var scope = new Panel(); - scope.Children.AddRange(grids); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; - - Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); - root.SetValue(Grid.IsSharedSizeScopeProperty, false); - Assert.All(grids, g => Assert.False(g.HasSharedSizeScope())); - Assert.Equal(null, root.GetValue(Grid.s_sharedSizeScopeHostProperty)); - } - - [Fact] - public void Size_Is_Propagated_Between_Grids() - { - var grids = new[] { CreateGrid("A", null),CreateGrid(("A",new GridLength(30)), (null, new GridLength()))}; - var scope = new Panel(); - scope.Children.AddRange(grids); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; - - root.Measure(new Size(50, 50)); - root.Arrange(new Rect(new Point(), new Point(50, 50))); - Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); - } - - [Fact] - public void Size_Propagation_Is_Constrained_To_Innermost_Scope() - { - var grids = new[] { CreateGrid("A", null), CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; - var innerScope = new Panel(); - innerScope.Children.AddRange(grids); - innerScope.SetValue(Grid.IsSharedSizeScopeProperty, true); - - var outerGrid = CreateGrid(("A", new GridLength(0))); - var outerScope = new Panel(); - outerScope.Children.AddRange(new[] { outerGrid, innerScope }); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = outerScope; - - root.Measure(new Size(50, 50)); - root.Arrange(new Rect(new Point(), new Point(50, 50))); - Assert.Equal(0, outerGrid.ColumnDefinitions[0].ActualWidth); - } - - [Fact] - public void Size_Is_Propagated_Between_Rows_And_Columns() - { - var grid = new Grid - { - ColumnDefinitions = new ColumnDefinitions("*,30"), - RowDefinitions = new RowDefinitions("*,10") - }; - - grid.ColumnDefinitions[1].SharedSizeGroup = "A"; - grid.RowDefinitions[1].SharedSizeGroup = "A"; - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = grid; - - root.Measure(new Size(50, 50)); - root.Arrange(new Rect(new Point(), new Point(50, 50))); - Assert.Equal(30, grid.RowDefinitions[1].ActualHeight); - } - - [Fact] - public void Size_Group_Changes_Are_Tracked() - { - var grids = new[] { - CreateGrid((null, new GridLength(0, GridUnitType.Auto)), (null, new GridLength())), - CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; - var scope = new Panel(); - scope.Children.AddRange(grids); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; - - root.Measure(new Size(50, 50)); - root.Arrange(new Rect(new Point(), new Point(50, 50))); - Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); - - grids[0].ColumnDefinitions[0].SharedSizeGroup = "A"; - - root.Measure(new Size(51, 51)); - root.Arrange(new Rect(new Point(), new Point(51, 51))); - Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); - - grids[0].ColumnDefinitions[0].SharedSizeGroup = null; - - root.Measure(new Size(52, 52)); - root.Arrange(new Rect(new Point(), new Point(52, 52))); - Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); - } - - [Fact] - public void Collection_Changes_Are_Tracked() - { - var grid = CreateGrid( - ("A", new GridLength(20)), - ("A", new GridLength(30)), - ("A", new GridLength(40)), - (null, new GridLength())); - - var scope = new Panel(); - scope.Children.Add(grid); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; - - grid.Measure(new Size(200, 200)); - grid.Arrange(new Rect(new Point(), new Point(200, 200))); - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(40, cd.ActualWidth)); - - grid.ColumnDefinitions.RemoveAt(2); - - grid.Measure(new Size(200, 200)); - grid.Arrange(new Rect(new Point(), new Point(200, 200))); - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); - - grid.ColumnDefinitions.Insert(1, new ColumnDefinition { Width = new GridLength(35), SharedSizeGroup = "A" }); - - grid.Measure(new Size(200, 200)); - grid.Arrange(new Rect(new Point(), new Point(200, 200))); - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(35, cd.ActualWidth)); - - grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(10), SharedSizeGroup = "A" }; - - grid.Measure(new Size(200, 200)); - grid.Arrange(new Rect(new Point(), new Point(200, 200))); - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); - - grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(50), SharedSizeGroup = "A" }; - - grid.Measure(new Size(200, 200)); - grid.Arrange(new Rect(new Point(), new Point(200, 200))); - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); - } - - [Fact] - public void Size_Priorities_Are_Maintained() - { - var sizers = new List(); - var grid = CreateGrid( - ("A", new GridLength(20)), - ("A", new GridLength(20, GridUnitType.Auto)), - ("A", new GridLength(1, GridUnitType.Star)), - ("A", new GridLength(1, GridUnitType.Star)), - (null, new GridLength())); - for (int i = 0; i < 3; i++) - sizers.Add(AddSizer(grid, i, 6 + i * 6)); - var scope = new Panel(); - scope.Children.Add(grid); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; - - grid.Measure(new Size(100, 100)); - grid.Arrange(new Rect(new Point(), new Point(100, 100))); - // all in group are equal to the first fixed column - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(20, cd.ActualWidth)); - - grid.ColumnDefinitions[0].SharedSizeGroup = null; - - grid.Measure(new Size(100, 100)); - grid.Arrange(new Rect(new Point(), new Point(100, 100))); - // all in group are equal to width (MinWidth) of the sizer in the second column - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 1 * 6, cd.ActualWidth)); - - grid.ColumnDefinitions[1].SharedSizeGroup = null; - - grid.Measure(new Size(double.PositiveInfinity, 100)); - grid.Arrange(new Rect(new Point(), new Point(100, 100))); - // with no constraint star columns default to the MinWidth of the sizer in the column - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 2 * 6, cd.ActualWidth)); - } - - // grid creators - private Grid CreateGrid(params string[] columnGroups) - { - return CreateGrid(columnGroups.Select(s => (s, ColumnDefinition.WidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); - } - - private Grid CreateGrid(params (string name, GridLength width)[] columns) - { - return CreateGrid(columns.Select(c => - (c.name, c.width, ColumnDefinition.MinWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); - } - - private Grid CreateGrid(params (string name, GridLength width, double minWidth)[] columns) - { - return CreateGrid(columns.Select(c => - (c.name, c.width, c.minWidth, ColumnDefinition.MaxWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); - } - - private Grid CreateGrid(params (string name, GridLength width, double minWidth, double maxWidth)[] columns) - { - var columnDefinitions = new ColumnDefinitions(); - - columnDefinitions.AddRange( - columns.Select(c => new ColumnDefinition - { - SharedSizeGroup = c.name, - Width = c.width, - MinWidth = c.minWidth, - MaxWidth = c.maxWidth - }) - ); - var grid = new Grid - { - ColumnDefinitions = columnDefinitions - }; - - return grid; - } - - private Control AddSizer(Grid grid, int column, double size = 30) - { - var ctrl = new Control { MinWidth = size, MinHeight = size }; - ctrl.SetValue(Grid.ColumnProperty,column); - grid.Children.Add(ctrl); - return ctrl; - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 0d87f6d0fe..932aada64e 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -385,6 +385,71 @@ namespace Avalonia.Controls.UnitTests Assert.True(target.SelectionEnd <= "123".Length); } } + [Fact] + public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123456789\r" + }; + target.CaretIndex = 11; + + Assert.True(true); + } + } + + [Fact] + public void TextBox_GotFocus_And_LostFocus_Work_Properly() + { + using (UnitTestApplication.Start(FocusServices)) + { + var target1 = new TextBox + { + Template = CreateTemplate(), + Text = "1234" + }; + var target2 = new TextBox + { + Template = CreateTemplate(), + Text = "5678" + }; + var sp = new StackPanel(); + sp.Children.Add(target1); + sp.Children.Add(target2); + + target1.ApplyTemplate(); + target2.ApplyTemplate(); + + var root = new TestRoot { Child = sp }; + + var gfcount = 0; + var lfcount = 0; + + target1.GotFocus += (s, e) => gfcount++; + target2.LostFocus += (s, e) => lfcount++; + + target2.Focus(); + Assert.False(target1.IsFocused); + Assert.True(target2.IsFocused); + + target1.Focus(); + Assert.False(target2.IsFocused); + Assert.True(target1.IsFocused); + + Assert.Equal(1, gfcount); + Assert.Equal(1, lfcount); + } + } + + private static TestServices FocusServices => TestServices.MockThreadingInterface.With( + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: new KeyboardNavigationHandler(), + inputManager: new InputManager(), + standardCursorFactory: Mock.Of()); private static TestServices Services => TestServices.MockThreadingInterface.With( standardCursorFactory: Mock.Of()); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 519872f9f2..b66d6ed11c 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.LogicalTree; using Avalonia.UnitTests; using Xunit; @@ -18,6 +19,8 @@ namespace Avalonia.Controls.UnitTests { public class TreeViewTests { + MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] public void Items_Should_Be_Created() { @@ -129,11 +132,7 @@ namespace Avalonia.Controls.UnitTests Assert.NotNull(container); - container.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(container); Assert.Equal(item, target.SelectedItem); Assert.True(container.IsSelected); @@ -164,12 +163,7 @@ namespace Avalonia.Controls.UnitTests Assert.True(container.IsSelected); - container.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - InputModifiers = InputModifiers.Control - }); + _mouse.Click(container, modifiers: InputModifiers.Control); Assert.Null(target.SelectedItem); Assert.False(container.IsSelected); @@ -204,13 +198,8 @@ namespace Avalonia.Controls.UnitTests Assert.True(container1.IsSelected); - container2.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - InputModifiers = InputModifiers.Control - }); - + _mouse.Click(container2, modifiers: InputModifiers.Control); + Assert.Equal(item2, target.SelectedItem); Assert.False(container1.IsSelected); Assert.True(container2.IsSelected); @@ -241,15 +230,15 @@ namespace Avalonia.Controls.UnitTests var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); - TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control); + ClickContainer(item1Container, InputModifiers.Control); Assert.True(item1Container.IsSelected); - TreeTestHelper.ClickContainer(item2Container, InputModifiers.Control); + ClickContainer(item2Container, InputModifiers.Control); Assert.True(item2Container.IsSelected); Assert.Equal(new[] {item1, item2}, target.SelectedItems.OfType()); - TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control); + ClickContainer(item1Container, InputModifiers.Control); Assert.False(item1Container.IsSelected); Assert.DoesNotContain(item1, target.SelectedItems.OfType()); @@ -280,12 +269,12 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(fromContainer, InputModifiers.None); Assert.True(fromContainer.IsSelected); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, InputModifiers.Shift); + AssertChildrenSelected(target, rootNode); } [Fact] @@ -313,12 +302,12 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(fromContainer, InputModifiers.None); Assert.True(fromContainer.IsSelected); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, InputModifiers.Shift); + AssertChildrenSelected(target, rootNode); } [Fact] @@ -346,12 +335,12 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(fromContainer, InputModifiers.None); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, InputModifiers.Shift); + AssertChildrenSelected(target, rootNode); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(fromContainer, InputModifiers.None); Assert.True(fromContainer.IsSelected); @@ -425,7 +414,6 @@ namespace Avalonia.Controls.UnitTests Assert.True(called); } - [Fact] public void LogicalChildren_Should_Be_Set() { @@ -623,6 +611,135 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Pressing_SelectAll_Gesture_Should_Select_All_Nodes() + { + using (UnitTestApplication.Start()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + var rootNode = tree[0]; + + var keymap = AvaloniaLocator.Current.GetService(); + var selectAllGesture = keymap.SelectAll.First(); + + var keyEvent = new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = selectAllGesture.Key, + Modifiers = selectAllGesture.Modifiers + }; + + target.RaiseEvent(keyEvent); + + AssertChildrenSelected(target, rootNode); + } + } + + [Fact] + public void Pressing_SelectAll_Gesture_With_Downward_Range_Selected_Should_Select_All_Nodes() + { + using (UnitTestApplication.Start()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + var rootNode = tree[0]; + + var from = rootNode.Children[0]; + var to = rootNode.Children.Last(); + + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + + ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(toContainer, InputModifiers.Shift); + + var keymap = AvaloniaLocator.Current.GetService(); + var selectAllGesture = keymap.SelectAll.First(); + + var keyEvent = new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = selectAllGesture.Key, + Modifiers = selectAllGesture.Modifiers + }; + + target.RaiseEvent(keyEvent); + + AssertChildrenSelected(target, rootNode); + } + } + + [Fact] + public void Pressing_SelectAll_Gesture_With_Upward_Range_Selected_Should_Select_All_Nodes() + { + using (UnitTestApplication.Start()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + var rootNode = tree[0]; + + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; + + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + + ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(toContainer, InputModifiers.Shift); + + var keymap = AvaloniaLocator.Current.GetService(); + var selectAllGesture = keymap.SelectAll.First(); + + var keyEvent = new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = selectAllGesture.Key, + Modifiers = selectAllGesture.Modifiers + }; + + target.RaiseEvent(keyEvent); + + AssertChildrenSelected(target, rootNode); + } + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); @@ -742,30 +859,23 @@ namespace Avalonia.Controls.UnitTests } } - private static class TreeTestHelper + void ClickContainer(IControl container, InputModifiers modifiers) { - public static void ClickContainer(IControl container, InputModifiers modifiers) - { - container.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - InputModifiers = modifiers - }); - } + _mouse.Click(container, modifiers: modifiers); + } - public static void AssertChildrenSelected(TreeView treeView, Node rootNode) + void AssertChildrenSelected(TreeView treeView, Node rootNode) + { + foreach (var child in rootNode.Children) { - foreach (var child in rootNode.Children) - { - var container = (TreeViewItem)treeView.ItemContainerGenerator.Index.ContainerFromItem(child); + var container = (TreeViewItem)treeView.ItemContainerGenerator.Index.ContainerFromItem(child); - Assert.True(container.IsSelected); - } + Assert.True(container.IsSelected); } } - private class Node : NotifyingBase + + private class Node : NotifyingBase { private IAvaloniaList _children; diff --git a/tests/Avalonia.DesignerSupport.TestApp/App.xaml b/tests/Avalonia.DesignerSupport.TestApp/App.xaml index 214bcd8797..56c0e8059a 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/App.xaml +++ b/tests/Avalonia.DesignerSupport.TestApp/App.xaml @@ -1,6 +1,8 @@ - + - \ No newline at end of file + diff --git a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj index ebe64b89ce..6bba5e7348 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj +++ b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj @@ -31,6 +31,6 @@ - + diff --git a/tests/Avalonia.DesignerSupport.TestApp/MainWindow.xaml b/tests/Avalonia.DesignerSupport.TestApp/MainWindow.xaml index 6938bd8c49..f90e5beaa6 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/MainWindow.xaml +++ b/tests/Avalonia.DesignerSupport.TestApp/MainWindow.xaml @@ -1,5 +1,7 @@  + Title="TESTAPP" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="Avalonia.DesignerSupport.TestApp.MainWindow">