From 2033a43b9b904573483173b97d7b8956a4d47eed Mon Sep 17 00:00:00 2001 From: ahopper Date: Fri, 13 Sep 2019 11:16:36 +0100 Subject: [PATCH 001/126] fix pointerover on mouse leaving window --- src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9bd58c10bc..45daeaaef9 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -619,7 +619,7 @@ namespace Avalonia.Win32 timestamp, _owner, RawPointerEventType.LeaveWindow, - new Point(), WindowsKeyboardDevice.Instance.Modifiers); + new Point(-1,-1), WindowsKeyboardDevice.Instance.Modifiers); break; case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN: @@ -634,7 +634,7 @@ namespace Avalonia.Win32 : msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN ? RawPointerEventType.RightButtonDown : RawPointerEventType.MiddleButtonDown, - new Point(0, 0), GetMouseModifiers(wParam)); + PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam)); break; case WindowsMessage.WM_TOUCH: var touchInputs = new TOUCHINPUT[wParam.ToInt32()]; From f29aeffecd7e95d2d63669b648c7a8580c58ffaa Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 3 Oct 2019 18:01:04 +0200 Subject: [PATCH 002/126] Avoid boxing mouse modifiers. --- src/Avalonia.Base/EnumExtensions.cs | 23 +++++++++++++++++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 12 ++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Base/EnumExtensions.cs diff --git a/src/Avalonia.Base/EnumExtensions.cs b/src/Avalonia.Base/EnumExtensions.cs new file mode 100644 index 0000000000..a8306c2d69 --- /dev/null +++ b/src/Avalonia.Base/EnumExtensions.cs @@ -0,0 +1,23 @@ +// 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.CompilerServices; + +namespace Avalonia +{ + /// + /// Provides extension methods for enums. + /// + public static class EnumExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool HasFlagCustom(this T value, T flag) where T : unmanaged, Enum + { + var intValue = *(int*)&value; + var intFlag = *(int*)&flag; + + return (intValue & intFlag) == intFlag; + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e8c3177ec5..6a733356b8 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -646,9 +646,9 @@ namespace Avalonia.Win32 { Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, _owner, - touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ? + touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ? RawPointerEventType.TouchEnd : - touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ? + touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ? RawPointerEventType.TouchBegin : RawPointerEventType.TouchUpdate, PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)), @@ -768,11 +768,11 @@ namespace Avalonia.Win32 { var keys = (UnmanagedMethods.ModifierKeys)ToInt32(wParam); var modifiers = WindowsKeyboardDevice.Instance.Modifiers; - if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) + if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) modifiers |= RawInputModifiers.LeftMouseButton; - if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) + if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) modifiers |= RawInputModifiers.RightMouseButton; - if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) + if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) modifiers |= RawInputModifiers.MiddleMouseButton; return modifiers; } @@ -782,7 +782,7 @@ namespace Avalonia.Win32 // Ensure that the delegate doesn't get garbage collected by storing it as a field. _wndProcDelegate = new UnmanagedMethods.WndProc(WndProc); - _className = "Avalonia-" + Guid.NewGuid(); + _className = $"Avalonia-{Guid.NewGuid().ToString()}"; UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX { From 09e76184631125508d834b04f45e0c707a709ce5 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 3 Oct 2019 18:24:28 +0200 Subject: [PATCH 003/126] Remove boxing from PointerPointProperties. --- src/Avalonia.Input/PointerPoint.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index d823a78090..55c1889e58 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + namespace Avalonia.Input { public sealed class PointerPoint @@ -27,9 +30,9 @@ namespace Avalonia.Input public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind) { PointerUpdateKind = kind; - IsLeftButtonPressed = modifiers.HasFlag(RawInputModifiers.LeftMouseButton); - IsMiddleButtonPressed = modifiers.HasFlag(RawInputModifiers.MiddleMouseButton); - IsRightButtonPressed = modifiers.HasFlag(RawInputModifiers.RightMouseButton); + IsLeftButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.LeftMouseButton); + IsMiddleButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.MiddleMouseButton); + IsRightButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.RightMouseButton); // The underlying input source might be reporting the previous state, // so make sure that we reflect the current state From 389d58f550cc5f5d4ba8e8883bd96921b0dddf23 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 20 Oct 2019 14:23:28 +0300 Subject: [PATCH 004/126] issue #3089 add failing tests --- .../Primitives/SelectingItemsControlTests.cs | 128 +++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 17f0e609a5..ce333f66d6 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.UnitTests.Primitives public class SelectingItemsControlTests { private MouseTestHelper _helper = new MouseTestHelper(); - + [Fact] public void SelectedIndex_Should_Initially_Be_Minus_1() { @@ -168,6 +168,130 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal("B", listBox.SelectedItem); } + [Fact] + public void Setting_SelectedIndex_Before_Initialize_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single, + Items = new[] { "foo", "bar", "baz" }, + SelectedIndex = 1 + }; + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + + [Fact] + public void Setting_SelectedIndex_During_Initialize_Should_Take_Priority_Over_Previous_Value() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single, + Items = new[] { "foo", "bar", "baz" }, + SelectedIndex = 2 + }; + + listBox.BeginInit(); + + listBox.SelectedIndex = 1; + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + + [Fact] + public void Setting_SelectedItem_Before_Initialize_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single, + Items = new[] { "foo", "bar", "baz" }, + SelectedItem = "bar" + }; + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + + + [Fact] + public void Setting_SelectedItems_Before_Initialize_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Multiple, + Items = new[] { "foo", "bar", "baz" }, + }; + + var selected = new[] { "foo", "bar" }; + + foreach (var v in selected) + { + listBox.SelectedItems.Add(v); + } + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(selected, listBox.SelectedItems); + } + + [Fact] + public void Setting_SelectedItems_During_Initialize_Should_Take_Priority_Over_Previous_Value() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Multiple, + Items = new[] { "foo", "bar", "baz" }, + }; + + var selected = new[] { "foo", "bar" }; + + foreach (var v in new[] { "bar", "baz" }) + { + listBox.SelectedItems.Add(v); + } + + listBox.BeginInit(); + + listBox.SelectedItems = new AvaloniaList(selected); + + listBox.EndInit(); + + Assert.Equal(selected, listBox.SelectedItems); + } + + [Fact] + public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single | SelectionMode.AlwaysSelected, + + Items = new[] { "foo", "bar", "baz" }, + SelectedIndex = 1 + }; + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + [Fact] public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelected_True() { @@ -849,7 +973,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var target = new ListBox { Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz"}, + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, }; target.ApplyTemplate(); From 2e70be023b4a5236d080be36b11572866acf0271 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 20 Oct 2019 14:36:23 +0300 Subject: [PATCH 005/126] fixed #3089 now set selecteditem/s before initialization should work properly --- .../Primitives/SelectingItemsControl.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7fddee1012..4b33b18475 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1088,9 +1088,15 @@ namespace Avalonia.Controls.Primitives } else { - SelectedIndex = _updateSelectedIndex != int.MinValue ? - _updateSelectedIndex : - AlwaysSelected ? 0 : -1; + if (_updateSelectedIndex != int.MinValue) + { + SelectedIndex = _updateSelectedIndex; + } + + if (AlwaysSelected && SelectedIndex == -1) + { + SelectedIndex = 0; + } } } } From ef0464a7e76dfe2182782c03786e7258ecf34479 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 25 Oct 2019 11:10:37 +0300 Subject: [PATCH 006/126] add failing tests for grid: when column/row (width/height ...) properties are changed grid doesn't react --- .../Avalonia.Controls.UnitTests/GridTests.cs | 122 ++++++++++++++++-- 1 file changed, 113 insertions(+), 9 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 2b9197e20b..353e5ecd42 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1,11 +1,6 @@ +using System; 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; @@ -34,7 +29,6 @@ namespace Avalonia.Controls.UnitTests 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 { @@ -1270,11 +1264,11 @@ namespace Avalonia.Controls.UnitTests // 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; @@ -1382,6 +1376,116 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(100, 100), grid.Bounds.Size); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Change_Column_Width_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) + { + var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; + + Change_Propery_And_Verify_Measure_Requested(grid, () => + { + if (setUsingAvaloniaProperty) + grid.ColumnDefinitions[0][ColumnDefinition.WidthProperty] = new GridLength(5); + else + grid.ColumnDefinitions[0].Width = new GridLength(5); + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Change_Column_MinWidth_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) + { + var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; + + Change_Propery_And_Verify_Measure_Requested(grid, () => + { + if (setUsingAvaloniaProperty) + grid.ColumnDefinitions[0][ColumnDefinition.MinWidthProperty] = 5; + else + grid.ColumnDefinitions[0].MinWidth = 5; + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Change_Column_MaxWidth_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) + { + var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; + + Change_Propery_And_Verify_Measure_Requested(grid, () => + { + if (setUsingAvaloniaProperty) + grid.ColumnDefinitions[0][ColumnDefinition.MaxWidthProperty] = 5; + else + grid.ColumnDefinitions[0].MaxWidth = 5; + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Change_Row_Height_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) + { + var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; + + Change_Propery_And_Verify_Measure_Requested(grid, () => + { + if (setUsingAvaloniaProperty) + grid.RowDefinitions[0][RowDefinition.HeightProperty] = new GridLength(5); + else + grid.RowDefinitions[0].Height = new GridLength(5); + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Change_Row_MinHeight_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) + { + var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; + + Change_Propery_And_Verify_Measure_Requested(grid, () => + { + if (setUsingAvaloniaProperty) + grid.RowDefinitions[0][RowDefinition.MinHeightProperty] = 5; + else + grid.RowDefinitions[0].MinHeight = 5; + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Change_Row_MaxHeight_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) + { + var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; + + Change_Propery_And_Verify_Measure_Requested(grid, () => + { + if (setUsingAvaloniaProperty) + grid.RowDefinitions[0][RowDefinition.MaxHeightProperty] = 5; + else + grid.RowDefinitions[0].MaxHeight = 5; + }); + } + + private static void Change_Propery_And_Verify_Measure_Requested(Grid grid, Action change) + { + grid.Measure(new Size(100, 100)); + grid.Arrange(new Rect(grid.DesiredSize)); + + Assert.True(grid.IsMeasureValid); + Assert.True(grid.IsArrangeValid); + + change(); + + Assert.False(grid.IsMeasureValid); + Assert.False(grid.IsArrangeValid); + } + private class TestControl : Control { public Size MeasureSize { get; set; } From 4f82615868d86e815631d08901284937666d835a Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 25 Oct 2019 15:11:23 +0300 Subject: [PATCH 007/126] fix problem when grid row/col properties (width/height ...) are changed grid to invalidate calculations --- src/Avalonia.Controls/ColumnDefinition.cs | 11 ++++++++--- src/Avalonia.Controls/DefinitionBase.cs | 17 +++++++++++++++++ src/Avalonia.Controls/RowDefinition.cs | 11 ++++++++--- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index 9c520c434e..6cad357e93 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -26,6 +26,14 @@ namespace Avalonia.Controls public static readonly StyledProperty WidthProperty = AvaloniaProperty.Register(nameof(Width), new GridLength(1, GridUnitType.Star)); + /// + /// Initializes static members of the class. + /// + static ColumnDefinition() + { + AffectsParentMeasure(WidthProperty, MinWidthProperty, MaxWidthProperty); + } + /// /// Initializes a new instance of the class. /// @@ -68,7 +76,6 @@ namespace Avalonia.Controls } set { - Parent?.InvalidateMeasure(); SetValue(MaxWidthProperty, value); } } @@ -84,7 +91,6 @@ namespace Avalonia.Controls } set { - Parent?.InvalidateMeasure(); SetValue(MinWidthProperty, value); } } @@ -100,7 +106,6 @@ namespace Avalonia.Controls } set { - Parent?.InvalidateMeasure(); SetValue(WidthProperty, value); } } diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index a68fe1265f..37c77ddb9c 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -730,5 +730,22 @@ namespace Avalonia.Controls SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); PrivateSharedSizeScopeProperty.Changed.AddClassHandler(OnPrivateSharedSizeScopePropertyChanged); } + + /// + /// Marks a property on a definition as affecting the parent grid's measurement. + /// + /// The properties. + protected static void AffectsParentMeasure(params AvaloniaProperty[] properties) + { + void Invalidate(AvaloniaPropertyChangedEventArgs e) + { + (e.Sender as DefinitionBase)?.Parent?.InvalidateMeasure(); + } + + foreach (var property in properties) + { + property.Changed.Subscribe(Invalidate); + } + } } } diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs index 1f2f738670..1a1a7e770b 100644 --- a/src/Avalonia.Controls/RowDefinition.cs +++ b/src/Avalonia.Controls/RowDefinition.cs @@ -26,6 +26,14 @@ namespace Avalonia.Controls public static readonly StyledProperty HeightProperty = AvaloniaProperty.Register(nameof(Height), new GridLength(1, GridUnitType.Star)); + /// + /// Initializes static members of the class. + /// + static RowDefinition() + { + AffectsParentMeasure(HeightProperty, MaxHeightProperty, MinHeightProperty); + } + /// /// Initializes a new instance of the class. /// @@ -68,7 +76,6 @@ namespace Avalonia.Controls } set { - Parent?.InvalidateMeasure(); SetValue(MaxHeightProperty, value); } } @@ -84,7 +91,6 @@ namespace Avalonia.Controls } set { - Parent?.InvalidateMeasure(); SetValue(MinHeightProperty, value); } } @@ -100,7 +106,6 @@ namespace Avalonia.Controls } set { - Parent?.InvalidateMeasure(); SetValue(HeightProperty, value); } } From fe4866d763539e0471785b80f705e58a1def2104 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 25 Oct 2019 15:04:05 +0300 Subject: [PATCH 008/126] issue: #3141 add failing tests for grid: when col/row are added grid doesn't trigger layout update --- .../Avalonia.Controls.UnitTests/GridTests.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 353e5ecd42..84ae12984c 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1472,6 +1472,27 @@ namespace Avalonia.Controls.UnitTests }); } + [Fact] + public void Add_Column_Should_Invalidate_Grid() + { + var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; + + Change_Propery_And_Verify_Measure_Requested(grid, () => + { + grid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(5))); + }); + } + + [Fact] + public void Add_Row_Should_Invalidate_Grid() + { + var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; + + Change_Propery_And_Verify_Measure_Requested(grid, () => + { + grid.RowDefinitions.Add(new RowDefinition(new GridLength(5))); + }); + } private static void Change_Propery_And_Verify_Measure_Requested(Grid grid, Action change) { grid.Measure(new Size(100, 100)); From acf0d42a8139b146859fb1b9cab19e9c8f6afba1 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 25 Oct 2019 15:14:17 +0300 Subject: [PATCH 009/126] fix issue: #3141 when col/row are added grid trigger layout update --- src/Avalonia.Controls/DefinitionBase.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 37c77ddb9c..3052728222 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -50,6 +50,8 @@ namespace Avalonia.Controls } } } + + Parent?.InvalidateMeasure(); } /// From b0eb2a10187d1cb60a359994bd2b22146ba643fd Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 25 Oct 2019 15:07:49 +0300 Subject: [PATCH 010/126] issue: #3141 add failing tests for grid: when col/row are removed grid doesn't trigger layout update --- .../Avalonia.Controls.UnitTests/GridTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 84ae12984c..187255a89b 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1493,6 +1493,28 @@ namespace Avalonia.Controls.UnitTests grid.RowDefinitions.Add(new RowDefinition(new GridLength(5))); }); } + + [Fact] + public void Remove_Column_Should_Invalidate_Grid() + { + var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; + + Change_Propery_And_Verify_Measure_Requested(grid, () => + { + grid.ColumnDefinitions.RemoveAt(0); + }); + } + + [Fact] + public void Remove_Row_Should_Invalidate_Grid() + { + var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; + + Change_Propery_And_Verify_Measure_Requested(grid, () => + { + grid.RowDefinitions.RemoveAt(0); + }); + } private static void Change_Propery_And_Verify_Measure_Requested(Grid grid, Action change) { grid.Measure(new Size(100, 100)); From 4b275a8f29c70106cdf57ab19d6ef4505f4a24a6 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 25 Oct 2019 15:14:55 +0300 Subject: [PATCH 011/126] fixed issue: #3141 when col/row are removed grid trigger layout update --- src/Avalonia.Controls/DefinitionBase.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 3052728222..6ad32f080a 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -65,6 +65,8 @@ namespace Avalonia.Controls _sharedState.RemoveMember(this); _sharedState = null; } + + Parent?.InvalidateMeasure(); } /// From a7f0851efb98d3f23b17d5eb4c8d46b38dddc916 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 25 Oct 2019 15:37:52 +0300 Subject: [PATCH 012/126] add failing test for grid when columns/row are replaced grid doesn't invalidate --- .../Avalonia.Controls.UnitTests/GridTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 187255a89b..192b8b2172 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1494,6 +1494,28 @@ namespace Avalonia.Controls.UnitTests }); } + [Fact] + public void Replace_Columns_Should_Invalidate_Grid() + { + var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; + + Change_Propery_And_Verify_Measure_Requested(grid, () => + { + grid.ColumnDefinitions = ColumnDefinitions.Parse("2*,1*"); + }); + } + + [Fact] + public void Replace_Rows_Should_Invalidate_Grid() + { + var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; + + Change_Propery_And_Verify_Measure_Requested(grid, () => + { + grid.RowDefinitions = RowDefinitions.Parse("2*,1*"); + }); + } + [Fact] public void Remove_Column_Should_Invalidate_Grid() { @@ -1515,6 +1537,7 @@ namespace Avalonia.Controls.UnitTests grid.RowDefinitions.RemoveAt(0); }); } + private static void Change_Propery_And_Verify_Measure_Requested(Grid grid, Action change) { grid.Measure(new Size(100, 100)); From 7f2b3261b4db52c1bea46c4e228a0a48cb22e778 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 25 Oct 2019 15:38:28 +0300 Subject: [PATCH 013/126] fix grid to invalidate when columns/rows are replaced --- src/Avalonia.Controls/Grid.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 8ecfe349f8..ef202178df 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -178,6 +178,7 @@ namespace Avalonia.Controls if (_data == null) { _data = new ExtendedData(); } _data.ColumnDefinitions = value; _data.ColumnDefinitions.Parent = this; + InvalidateMeasure(); } } @@ -198,6 +199,7 @@ namespace Avalonia.Controls if (_data == null) { _data = new ExtendedData(); } _data.RowDefinitions = value; _data.RowDefinitions.Parent = this; + InvalidateMeasure(); } } From 56cfbc1e465e25efee7410edd6c021c9b2ab8404 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 25 Oct 2019 16:13:35 +0300 Subject: [PATCH 014/126] add failing test for issue #3155 add/remove grid child runtime throws exception --- .../Avalonia.Controls.UnitTests/GridTests.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 192b8b2172..5db28238f4 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1538,6 +1538,69 @@ namespace Avalonia.Controls.UnitTests }); } + [Fact] + public void Remove_Child_Should_Invalidate_Grid_And_Be_Operational() + { + var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("*,Auto") }; + + grid.Children.Add(new Decorator() { [Grid.ColumnProperty] = 0 }); + grid.Children.Add(new Decorator() { Width = 10, Height = 10, [Grid.ColumnProperty] = 1 }); + + var size = new Size(100, 100); + grid.Measure(size); + grid.Arrange(new Rect(size)); + + Assert.True(grid.IsMeasureValid); + Assert.True(grid.IsArrangeValid); + + Assert.Equal(90, grid.Children[0].Bounds.Width); + Assert.Equal(10, grid.Children[1].Bounds.Width); + + grid.Children.RemoveAt(1); + + Assert.False(grid.IsMeasureValid); + Assert.False(grid.IsArrangeValid); + + grid.Measure(size); + grid.Arrange(new Rect(size)); + + Assert.True(grid.IsMeasureValid); + Assert.True(grid.IsArrangeValid); + + Assert.Equal(100, grid.Children[0].Bounds.Width); + } + + [Fact] + public void Add_Child_Should_Invalidate_Grid_And_Be_Operational() + { + var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("*,Auto") }; + + grid.Children.Add(new Decorator() { [Grid.ColumnProperty] = 0 }); + + var size = new Size(100, 100); + grid.Measure(size); + grid.Arrange(new Rect(size)); + + Assert.True(grid.IsMeasureValid); + Assert.True(grid.IsArrangeValid); + + Assert.Equal(100, grid.Children[0].Bounds.Width); + + grid.Children.Add(new Decorator() { Width = 10, Height = 10, [Grid.ColumnProperty] = 1 }); + + Assert.False(grid.IsMeasureValid); + Assert.False(grid.IsArrangeValid); + + grid.Measure(size); + grid.Arrange(new Rect(size)); + + Assert.True(grid.IsMeasureValid); + Assert.True(grid.IsArrangeValid); + + Assert.Equal(90, grid.Children[0].Bounds.Width); + Assert.Equal(10, grid.Children[1].Bounds.Width); + } + private static void Change_Propery_And_Verify_Measure_Requested(Grid grid, Action change) { grid.Measure(new Size(100, 100)); From d478572e7452a5d79625ab42f39b75d32aa2d2c8 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 25 Oct 2019 16:14:36 +0300 Subject: [PATCH 015/126] fix issue #3155 add/remove grid child runtime works properly --- src/Avalonia.Controls/Grid.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index ef202178df..23c1cd4794 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -6,6 +6,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using System.Threading; @@ -571,6 +572,15 @@ namespace Avalonia.Controls return (arrangeSize); } + /// + /// + /// + protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + { + CellsStructureDirty = true; + base.ChildrenChanged(sender, e); + } + /// /// Invalidates grid caches and makes the grid dirty for measure. /// From 3b1005f04e78cee9928ae332a695d20f5b7bdd71 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 25 Oct 2019 16:39:13 +0300 Subject: [PATCH 016/126] Removed pointer global state that caused trouble --- src/Avalonia.Input/MouseDevice.cs | 13 +++++++-- src/Avalonia.Input/TouchDevice.cs | 21 ++++++++++++--- src/Avalonia.Native/AvaloniaNativePlatform.cs | 1 - src/Avalonia.Native/WindowImplBase.cs | 5 ++-- src/Avalonia.X11/X11Platform.cs | 2 -- src/Avalonia.X11/X11Window.cs | 10 +++++-- src/Avalonia.X11/XI2Manager.cs | 12 ++++----- .../Input/WindowsMouseDevice.cs | 8 ------ src/Windows/Avalonia.Win32/WindowImpl.cs | 27 ++++++++++--------- 9 files changed, 60 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index c84596b913..4dcf0eee53 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -14,13 +14,14 @@ namespace Avalonia.Input /// /// Represents a mouse device. /// - public class MouseDevice : IMouseDevice + public class MouseDevice : IMouseDevice, IDisposable { private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; private readonly Pointer _pointer; + private bool _disposed; public MouseDevice(Pointer pointer = null) { @@ -126,7 +127,9 @@ namespace Avalonia.Input { Contract.Requires(e != null); - var mouse = (IMouseDevice)e.Device; + var mouse = (MouseDevice)e.Device; + if(mouse._disposed) + return; Position = e.Root.PointToScreen(e.Position); var props = CreateProperties(e); @@ -441,5 +444,11 @@ namespace Avalonia.Input el = (IInputElement)el.VisualParent; } } + + public void Dispose() + { + _disposed = true; + _pointer?.Dispose(); + } } } diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index b231c9fff4..d6ad836f37 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Avalonia.Input.Raw; @@ -11,10 +12,11 @@ namespace Avalonia.Input /// This class is supposed to be used on per-toplevel basis, don't use a shared one /// /// - public class TouchDevice : IInputDevice + public class TouchDevice : IInputDevice, IDisposable { - Dictionary _pointers = new Dictionary(); - + private readonly Dictionary _pointers = new Dictionary(); + private bool _disposed; + KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); @@ -28,6 +30,8 @@ namespace Avalonia.Input public void ProcessRawEvent(RawInputEventArgs ev) { + if(_disposed) + return; var args = (RawTouchEventArgs)ev; if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) { @@ -82,6 +86,17 @@ namespace Avalonia.Input } + + public void Dispose() + { + if(_disposed) + return; + var values = _pointers.Values.ToList(); + _pointers.Clear(); + _disposed = true; + foreach (var p in values) + p.Dispose(); + } } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index ddb71b61bb..fab3ce36b8 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -95,7 +95,6 @@ namespace Avalonia.Native .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind().ToSingleton() .Bind().ToConstant(KeyboardDevice) - .Bind().ToConstant(MouseDevice) .Bind().ToConstant(this) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index fe7458d583..7f1fab4b1c 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -24,7 +24,7 @@ namespace Avalonia.Native private object _syncRoot = new object(); private bool _deferredRendering = false; private bool _gpu = false; - private readonly IMouseDevice _mouse; + private readonly MouseDevice _mouse; private readonly IKeyboardDevice _keyboard; private readonly IStandardCursorFactory _cursorFactory; private Size _savedLogicalSize; @@ -38,7 +38,7 @@ namespace Avalonia.Native _deferredRendering = opts.UseDeferredRendering; _keyboard = AvaloniaLocator.Current.GetService(); - _mouse = AvaloniaLocator.Current.GetService(); + _mouse = new MouseDevice(); _cursorFactory = AvaloniaLocator.Current.GetService(); } @@ -142,6 +142,7 @@ namespace Avalonia.Native { n?.Dispose(); } + _parent._mouse.Dispose(); } void IAvnWindowBaseEvents.Activated() => _parent.Activated?.Invoke(); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index d7a7bb97fd..6ba562bb69 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -19,9 +19,7 @@ namespace Avalonia.X11 class AvaloniaX11Platform : IWindowingPlatform { private Lazy _keyboardDevice = new Lazy(() => new KeyboardDevice()); - private Lazy _mouseDevice = new Lazy(() => new MouseDevice()); public KeyboardDevice KeyboardDevice => _keyboardDevice.Value; - public MouseDevice MouseDevice => _mouseDevice.Value; public Dictionary> Windows = new Dictionary>(); public XI2Manager XI2; public X11Info Info { get; private set; } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 17471fad10..32460fed86 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -32,7 +32,8 @@ namespace Avalonia.X11 private PixelPoint? _configurePoint; private bool _triggeredExpose; private IInputRoot _inputRoot; - private readonly IMouseDevice _mouse; + private readonly MouseDevice _mouse; + private readonly TouchDevice _touch; private readonly IKeyboardDevice _keyboard; private PixelPoint? _position; private PixelSize _realSize; @@ -57,7 +58,8 @@ namespace Avalonia.X11 _platform = platform; _popup = popupParent != null; _x11 = platform.Info; - _mouse = platform.MouseDevice; + _mouse = new MouseDevice(); + _touch = new TouchDevice(); _keyboard = platform.KeyboardDevice; var glfeature = AvaloniaLocator.Current.GetService(); @@ -702,6 +704,8 @@ namespace Avalonia.X11 _platform.XI2?.OnWindowDestroyed(_handle); _handle = IntPtr.Zero; Closed?.Invoke(); + _mouse.Dispose(); + _touch.Dispose(); } if (_useRenderWindow && _renderHandle != IntPtr.Zero) @@ -830,6 +834,8 @@ namespace Avalonia.X11 } public IMouseDevice MouseDevice => _mouse; + public TouchDevice TouchDevice => _touch; + public IPopupImpl CreatePopup() => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this); diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index 6989d6d26d..e37ed39bee 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -92,8 +92,6 @@ namespace Avalonia.X11 private PointerDeviceInfo _pointerDevice; private AvaloniaX11Platform _platform; - private readonly TouchDevice _touchDevice = new TouchDevice(); - public bool Init(AvaloniaX11Platform platform) { @@ -198,7 +196,7 @@ namespace Avalonia.X11 (ev.Type == XiEventType.XI_TouchUpdate ? RawPointerEventType.TouchUpdate : RawPointerEventType.TouchEnd); - client.ScheduleInput(new RawTouchEventArgs(_touchDevice, + client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice, ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail)); return; } @@ -232,10 +230,10 @@ namespace Avalonia.X11 } if (scrollDelta != default) - client.ScheduleInput(new RawMouseWheelEventArgs(_platform.MouseDevice, ev.Timestamp, + client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, ev.Position, scrollDelta, ev.Modifiers)); if (_pointerDevice.HasMotion(ev)) - client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, + client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, RawPointerEventType.Move, ev.Position, ev.Modifiers)); } @@ -248,7 +246,7 @@ namespace Avalonia.X11 : ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp) : (RawPointerEventType?)null; if (type.HasValue) - client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, + client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, type.Value, ev.Position, ev.Modifiers)); } @@ -310,5 +308,7 @@ namespace Avalonia.X11 { IInputRoot InputRoot { get; } void ScheduleInput(RawInputEventArgs args); + IMouseDevice MouseDevice { get; } + TouchDevice TouchDevice { get; } } } diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs index e7c379ad89..8f060b1b81 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs @@ -11,19 +11,11 @@ namespace Avalonia.Win32.Input { class WindowsMouseDevice : MouseDevice { - public static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice(); - public WindowsMouseDevice() : base(new WindowsMousePointer()) { } - public WindowImpl CurrentWindow - { - get; - set; - } - class WindowsMousePointer : Pointer { public WindowsMousePointer() : base(Pointer.GetNextFreeId(),PointerType.Mouse, true) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 04a9303d53..3777ce5e1e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -30,6 +30,7 @@ namespace Avalonia.Win32 private IntPtr _hwnd; private bool _multitouch; private TouchDevice _touchDevice = new TouchDevice(); + private MouseDevice _mouseDevice = new WindowsMouseDevice(); private IInputRoot _owner; private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock(); private bool _trackingMouse; @@ -205,7 +206,7 @@ namespace Avalonia.Win32 } } - public IMouseDevice MouseDevice => WindowsMouseDevice.Instance; + public IMouseDevice MouseDevice => _mouseDevice; public WindowState WindowState { @@ -333,7 +334,7 @@ namespace Avalonia.Win32 public void BeginMoveDrag(PointerPressedEventArgs e) { - WindowsMouseDevice.Instance.Capture(null); + _mouseDevice.Capture(null); UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN, new IntPtr((int)UnmanagedMethods.HitTestValues.HTCAPTION), IntPtr.Zero); e.Pointer.Capture(null); @@ -356,7 +357,7 @@ namespace Avalonia.Win32 #if USE_MANAGED_DRAG _managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position)); #else - WindowsMouseDevice.Instance.Capture(null); + _mouseDevice.Capture(null); UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN, new IntPtr((int)EdgeDic[edge]), IntPtr.Zero); #endif @@ -437,9 +438,7 @@ namespace Avalonia.Win32 uint timestamp = unchecked((uint)UnmanagedMethods.GetMessageTime()); RawInputEventArgs e = null; - - WindowsMouseDevice.Instance.CurrentWindow = this; - + switch ((UnmanagedMethods.WindowsMessage)msg) { case UnmanagedMethods.WindowsMessage.WM_ACTIVATE: @@ -485,6 +484,8 @@ namespace Avalonia.Win32 _parent._disabledBy.Remove(this); _parent.UpdateEnabled(); } + _mouseDevice.Dispose(); + _touchDevice?.Dispose(); //Free other resources Dispose(); return IntPtr.Zero; @@ -542,7 +543,7 @@ namespace Avalonia.Win32 if(ShouldIgnoreTouchEmulatedMessage()) break; e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN @@ -559,7 +560,7 @@ namespace Avalonia.Win32 if(ShouldIgnoreTouchEmulatedMessage()) break; e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP @@ -587,7 +588,7 @@ namespace Avalonia.Win32 } e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, RawPointerEventType.Move, @@ -597,7 +598,7 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_MOUSEWHEEL: e = new RawMouseWheelEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, PointToClient(PointFromLParam(lParam)), @@ -606,7 +607,7 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_MOUSEHWHEEL: e = new RawMouseWheelEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, PointToClient(PointFromLParam(lParam)), @@ -616,7 +617,7 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_MOUSELEAVE: _trackingMouse = false; e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, RawPointerEventType.LeaveWindow, @@ -627,7 +628,7 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN: e = new RawPointerEventArgs( - WindowsMouseDevice.Instance, + _mouseDevice, timestamp, _owner, msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN From 3054a513ee33ec96c9220b57817721226522838f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 25 Oct 2019 16:44:36 +0300 Subject: [PATCH 017/126] Fix AppBuilderBase.Start --- src/Avalonia.Controls/AppBuilderBase.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index d9be9171ed..85eab3be7b 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -126,8 +126,7 @@ namespace Avalonia.Controls // Copy-pasted because we can't call extension methods due to generic constraints var lifetime = new ClassicDesktopStyleApplicationLifetime(Instance) {ShutdownMode = ShutdownMode.OnMainWindowClose}; - Instance.ApplicationLifetime = lifetime; - SetupWithoutStarting(); + SetupWithLifetime(lifetime); lifetime.Start(Array.Empty()); } From 171a75eaed1ce5b0bfa1245fdc2f98ff6c67017c Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 25 Oct 2019 16:46:37 +0300 Subject: [PATCH 018/126] pr comments --- .../Avalonia.Controls.UnitTests/GridTests.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 5db28238f4..b74dd641df 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1379,7 +1379,7 @@ namespace Avalonia.Controls.UnitTests [Theory] [InlineData(true)] [InlineData(false)] - public void Change_Column_Width_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) + public void Changing_Column_Width_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; @@ -1395,7 +1395,7 @@ namespace Avalonia.Controls.UnitTests [Theory] [InlineData(true)] [InlineData(false)] - public void Change_Column_MinWidth_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) + public void Changing_Column_MinWidth_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; @@ -1411,7 +1411,7 @@ namespace Avalonia.Controls.UnitTests [Theory] [InlineData(true)] [InlineData(false)] - public void Change_Column_MaxWidth_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) + public void Changing_Column_MaxWidth_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; @@ -1427,7 +1427,7 @@ namespace Avalonia.Controls.UnitTests [Theory] [InlineData(true)] [InlineData(false)] - public void Change_Row_Height_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) + public void Changing_Row_Height_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; @@ -1443,7 +1443,7 @@ namespace Avalonia.Controls.UnitTests [Theory] [InlineData(true)] [InlineData(false)] - public void Change_Row_MinHeight_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) + public void Changing_Row_MinHeight_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; @@ -1459,7 +1459,7 @@ namespace Avalonia.Controls.UnitTests [Theory] [InlineData(true)] [InlineData(false)] - public void Change_Row_MaxHeight_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) + public void Changing_Row_MaxHeight_Should_Invalidate_Grid(bool setUsingAvaloniaProperty) { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; @@ -1473,7 +1473,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Add_Column_Should_Invalidate_Grid() + public void Adding_Column_Should_Invalidate_Grid() { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; @@ -1484,7 +1484,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Add_Row_Should_Invalidate_Grid() + public void Adding_Row_Should_Invalidate_Grid() { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; @@ -1495,7 +1495,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Replace_Columns_Should_Invalidate_Grid() + public void Replacing_Columns_Should_Invalidate_Grid() { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; @@ -1506,7 +1506,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Replace_Rows_Should_Invalidate_Grid() + public void Replacing_Rows_Should_Invalidate_Grid() { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; @@ -1517,7 +1517,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Remove_Column_Should_Invalidate_Grid() + public void Removing_Column_Should_Invalidate_Grid() { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; @@ -1528,7 +1528,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Remove_Row_Should_Invalidate_Grid() + public void Removing_Row_Should_Invalidate_Grid() { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; @@ -1539,7 +1539,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Remove_Child_Should_Invalidate_Grid_And_Be_Operational() + public void Removing_Child_Should_Invalidate_Grid_And_Be_Operational() { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("*,Auto") }; @@ -1571,7 +1571,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Add_Child_Should_Invalidate_Grid_And_Be_Operational() + public void Adding_Child_Should_Invalidate_Grid_And_Be_Operational() { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("*,Auto") }; From d8ee7531aba3bb77dd07c60012ec117d6c287c5c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 25 Oct 2019 17:29:18 +0300 Subject: [PATCH 019/126] Properly raise PointerCaptureLost on capture transfer --- src/Avalonia.Input/Pointer.cs | 2 +- .../Avalonia.Input.UnitTests/PointerTests.cs | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.Input.UnitTests/PointerTests.cs diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index 80d803abb1..819d231b31 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/src/Avalonia.Input/Pointer.cs @@ -37,7 +37,7 @@ namespace Avalonia.Input { if (Captured != null) Captured.DetachedFromVisualTree -= OnCaptureDetached; - var oldCapture = control; + var oldCapture = Captured; Captured = control; PlatformCapture(control); if (oldCapture != null) diff --git a/tests/Avalonia.Input.UnitTests/PointerTests.cs b/tests/Avalonia.Input.UnitTests/PointerTests.cs new file mode 100644 index 0000000000..e639726dbd --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/PointerTests.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +namespace Avalonia.Input.UnitTests +{ + public class PointerTests + { + [Fact] + public void On_Capture_Transfer_PointerCaptureLost_Should_Propagate_Up_To_The_Common_Parent() + { + Border initialParent, initialCapture, newParent, newCapture; + var el = new StackPanel + { + Children = + { + (initialParent = new Border { Child = initialCapture = new Border() }), + (newParent = new Border { Child = newCapture = new Border() }) + } + }; + var receivers = new List(); + var root = new TestRoot(el); + foreach (InputElement d in root.GetSelfAndVisualDescendants()) + d.PointerCaptureLost += (s, e) => receivers.Add(s); + var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + + pointer.Capture(initialCapture); + pointer.Capture(newCapture); + Assert.True(receivers.SequenceEqual(new[] { initialCapture, initialParent })); + + receivers.Clear(); + pointer.Capture(null); + Assert.True(receivers.SequenceEqual(new object[] { newCapture, newParent, el, root })); + } + } +} From 66b3da044c7a8af634e14bdc4e15beb0071b5eb7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 25 Oct 2019 18:20:01 +0300 Subject: [PATCH 020/126] Fixed MouseTestHelper --- tests/Avalonia.UnitTests/MouseTestHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index 48c4d73471..f6454a9cd2 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -84,9 +84,9 @@ namespace Avalonia.UnitTests ); if (ButtonCount(props) == 0) { - _pointer.Capture(null); target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, Timestamp(), props, GetModifiers(modifiers), _pressedButton)); + _pointer.Capture(null); } else Move(target, source, position); From b9bd57e25bb99430622a7c2c91f2a4e9904b6c74 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 25 Oct 2019 19:09:48 +0300 Subject: [PATCH 021/126] Removed `Application` parameter from ClassicDesktopStyleApplicationLifetime since it wasn't used anyway --- src/Avalonia.Controls/AppBuilderBase.cs | 2 +- .../ClassicDesktopStyleApplicationLifetime.cs | 9 ++++----- .../DesktopStyleApplicationLifetimeTests.cs | 18 +++++++++--------- .../AutoSuspendHelperTest.cs | 6 +++--- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 85eab3be7b..3b644191c2 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -125,7 +125,7 @@ namespace Avalonia.Controls }); // Copy-pasted because we can't call extension methods due to generic constraints - var lifetime = new ClassicDesktopStyleApplicationLifetime(Instance) {ShutdownMode = ShutdownMode.OnMainWindowClose}; + var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = ShutdownMode.OnMainWindowClose}; SetupWithLifetime(lifetime); lifetime.Start(Array.Empty()); } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 2533191ae4..6dd5b8cc81 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -5,12 +5,12 @@ using System.Threading; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Interactivity; +using Avalonia.Threading; namespace Avalonia.Controls.ApplicationLifetimes { public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime, IDisposable { - private readonly Application _app; private int _exitCode; private CancellationTokenSource _cts; private bool _isShuttingDown; @@ -34,12 +34,11 @@ namespace Avalonia.Controls.ApplicationLifetimes _activeLifetime?._windows.Add((Window)sender); } - public ClassicDesktopStyleApplicationLifetime(Application app) + public ClassicDesktopStyleApplicationLifetime() { if (_activeLifetime != null) throw new InvalidOperationException( "Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed"); - _app = app; _activeLifetime = this; } @@ -103,7 +102,7 @@ namespace Avalonia.Controls.ApplicationLifetimes Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); _cts = new CancellationTokenSource(); MainWindow?.Show(); - _app.Run(_cts.Token); + Dispatcher.UIThread.MainLoop(_cts.Token); Environment.ExitCode = _exitCode; return _exitCode; } @@ -124,7 +123,7 @@ namespace Avalonia this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) where T : AppBuilderBase, new() { - var lifetime = new ClassicDesktopStyleApplicationLifetime(builder.Instance) {ShutdownMode = shutdownMode}; + var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = shutdownMode}; builder.SetupWithLifetime(lifetime); return lifetime.Start(args); } diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 74523d4193..dee7a84812 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -16,7 +16,7 @@ namespace Avalonia.Controls.UnitTests public void Should_Set_ExitCode_After_Shutdown() { using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) - using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { lifetime.Shutdown(1337); @@ -31,7 +31,7 @@ namespace Avalonia.Controls.UnitTests public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call() { using (UnitTestApplication.Start(TestServices.StyledWindow)) - using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { var windows = new List { new Window(), new Window(), new Window(), new Window() }; @@ -50,7 +50,7 @@ namespace Avalonia.Controls.UnitTests public void Should_Only_Exit_On_Explicit_Exit() { using (UnitTestApplication.Start(TestServices.StyledWindow)) - using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; @@ -84,7 +84,7 @@ namespace Avalonia.Controls.UnitTests public void Should_Exit_After_MainWindow_Closed() { using (UnitTestApplication.Start(TestServices.StyledWindow)) - using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; @@ -112,7 +112,7 @@ namespace Avalonia.Controls.UnitTests public void Should_Exit_After_Last_Window_Closed() { using (UnitTestApplication.Start(TestServices.StyledWindow)) - using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose; @@ -142,7 +142,7 @@ namespace Avalonia.Controls.UnitTests public void Show_Should_Add_Window_To_OpenWindows() { using (UnitTestApplication.Start(TestServices.StyledWindow)) - using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { var window = new Window(); @@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests public void Window_Should_Be_Added_To_OpenWindows_Only_Once() { using (UnitTestApplication.Start(TestServices.StyledWindow)) - using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { var window = new Window(); @@ -174,7 +174,7 @@ namespace Avalonia.Controls.UnitTests public void Close_Should_Remove_Window_From_OpenWindows() { using (UnitTestApplication.Start(TestServices.StyledWindow)) - using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { var window = new Window(); @@ -197,7 +197,7 @@ namespace Avalonia.Controls.UnitTests windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)); using (UnitTestApplication.Start(services)) - using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { var window = new Window(); diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs index 56b14c3936..c263a80ef3 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs @@ -45,7 +45,7 @@ namespace Avalonia.ReactiveUI.UnitTests public void AutoSuspendHelper_Should_Immediately_Fire_IsLaunchingNew() { using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) - using (var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + using (var lifetime = new ClassicDesktopStyleApplicationLifetime()) { var isLaunchingReceived = false; var application = AvaloniaLocator.Current.GetService(); @@ -86,7 +86,7 @@ namespace Avalonia.ReactiveUI.UnitTests public void ShouldPersistState_Should_Fire_On_App_Exit_When_SuspensionDriver_Is_Initialized() { using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) - using (var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + using (var lifetime = new ClassicDesktopStyleApplicationLifetime()) { var shouldPersistReceived = false; var application = AvaloniaLocator.Current.GetService(); @@ -105,4 +105,4 @@ namespace Avalonia.ReactiveUI.UnitTests } } } -} \ No newline at end of file +} From 50acbfec9f651efb58985f15e392f88b0f28a761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 26 Oct 2019 00:45:27 +0100 Subject: [PATCH 022/126] XML comment fixes. --- src/Avalonia.Controls/DesktopApplicationExtensions.cs | 1 + src/Avalonia.Controls/Primitives/PopupRoot.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/Avalonia.Controls/DesktopApplicationExtensions.cs b/src/Avalonia.Controls/DesktopApplicationExtensions.cs index ff6705cdc0..ddd4e57a40 100644 --- a/src/Avalonia.Controls/DesktopApplicationExtensions.cs +++ b/src/Avalonia.Controls/DesktopApplicationExtensions.cs @@ -50,6 +50,7 @@ namespace Avalonia.Controls /// On desktop-style platforms runs the application's main loop with custom CancellationToken /// without setting a lifetime. /// + /// The application. /// The token to track. public static void Run(this Application app, CancellationToken token) { diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 74a6d288f4..caaf58b25b 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -41,6 +41,8 @@ namespace Avalonia.Controls.Primitives /// /// Initializes a new instance of the class. /// + /// The popup parent. + /// The popup implementation. /// /// The dependency resolver to use. If null the default dependency resolver will be used. /// From 00263f71e4b7e3a9acb93444e07b10b5e35fa1a4 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 26 Oct 2019 16:49:10 +0200 Subject: [PATCH 023/126] Port GridSplitter from WPF. --- src/Avalonia.Controls/GridSplitter.cs | 872 +++++++++++++++--- src/Avalonia.Themes.Default/GridSplitter.xaml | 56 +- .../GridSplitterTests.cs | 313 ++++--- 3 files changed, 955 insertions(+), 286 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 28b9b3a38f..e5749c8ed9 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -1,210 +1,848 @@ -// 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.Generic; -using System.Linq; +using System.Diagnostics; +using Avalonia.Collections; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; -using Avalonia.VisualTree; +using Avalonia.Media; +using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// Represents the control that redistributes space between columns or rows of a Grid control. + /// Enum to indicate whether GridSplitter resizes Columns or Rows. + /// + public enum GridResizeDirection + { + /// + /// Determines whether to resize rows or columns based on its Alignment and + /// width compared to height. + /// + Auto, + + /// + /// Resize columns when dragging Splitter. + /// + Columns, + + /// + /// Resize rows when dragging Splitter. + /// + Rows + } + + /// + /// Enum to indicate what Columns or Rows the GridSplitter resizes. + /// + public enum GridResizeBehavior + { + /// + /// Determine which columns or rows to resize based on its Alignment. + /// + BasedOnAlignment, + + /// + /// Resize the current and next Columns or Rows. + /// + CurrentAndNext, + + /// + /// Resize the previous and current Columns or Rows. + /// + PreviousAndCurrent, + + /// + /// Resize the previous and next Columns or Rows. + /// + PreviousAndNext + } + + /// + /// Represents the control that redistributes space between columns or rows of a Grid control. /// - /// - /// Unlike WPF GridSplitter, Avalonia GridSplitter has only one Behavior, GridResizeBehavior.PreviousAndNext. - /// public class GridSplitter : Thumb { - private List _definitions; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty ResizeDirectionProperty = + AvaloniaProperty.Register(nameof(ResizeDirection)); - private Grid _grid; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty ResizeBehaviorProperty = + AvaloniaProperty.Register(nameof(ResizeBehavior)); - private DefinitionBase _nextDefinition; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty ShowsPreviewProperty = + AvaloniaProperty.Register(nameof(ShowsPreview)); - private Orientation _orientation; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty KeyboardIncrementProperty = + AvaloniaProperty.Register(nameof(KeyboardIncrement), 10d); - private DefinitionBase _prevDefinition; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty DragIncrementProperty = + AvaloniaProperty.Register(nameof(DragIncrement), 1d); - private void GetDeltaConstraints(out double min, out double max) + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty> PreviewContentProperty = + AvaloniaProperty.Register>(nameof(PreviewContent)); + + private static readonly Cursor s_columnSplitterCursor = new Cursor(StandardCursorType.SizeWestEast); + private static readonly Cursor s_rowSplitterCursor = new Cursor(StandardCursorType.SizeNorthSouth); + + private ResizeData _resizeData; + + /// + /// Indicates whether the Splitter resizes the Columns, Rows, or Both. + /// + public GridResizeDirection ResizeDirection { - var prevDefinitionLen = GetActualLength(_prevDefinition); - var prevDefinitionMin = GetMinLength(_prevDefinition); - var prevDefinitionMax = GetMaxLength(_prevDefinition); + get => GetValue(ResizeDirectionProperty); + set => SetValue(ResizeDirectionProperty, value); + } - var nextDefinitionLen = GetActualLength(_nextDefinition); - var nextDefinitionMin = GetMinLength(_nextDefinition); - var nextDefinitionMax = GetMaxLength(_nextDefinition); - // Determine the minimum and maximum the columns can be resized - min = -Math.Min(prevDefinitionLen - prevDefinitionMin, nextDefinitionMax - nextDefinitionLen); - max = Math.Min(prevDefinitionMax - prevDefinitionLen, nextDefinitionLen - nextDefinitionMin); + /// + /// Indicates which Columns or Rows the Splitter resizes. + /// + public GridResizeBehavior ResizeBehavior + { + get => GetValue(ResizeBehaviorProperty); + set => SetValue(ResizeBehaviorProperty, value); } - protected override void OnDragDelta(VectorEventArgs e) + /// + /// Indicates whether to Preview the column resizing without updating layout. + /// + public bool ShowsPreview + { + get => GetValue(ShowsPreviewProperty); + set => SetValue(ShowsPreviewProperty, value); + } + + /// + /// The Distance to move the splitter when pressing the keyboard arrow keys. + /// + public double KeyboardIncrement + { + get => GetValue(KeyboardIncrementProperty); + set => SetValue(KeyboardIncrementProperty, value); + } + + /// + /// Restricts splitter to move a multiple of the specified units. + /// + public double DragIncrement + { + get => GetValue(DragIncrementProperty); + set => SetValue(DragIncrementProperty, value); + } + + /// + /// Gets or sets content that will be shown when is enabled and user starts resize operation. + /// + public ITemplate PreviewContent { - // WPF doesn't change anything when spliter is in the last row/column - // but resizes the splitter row/column when it's the first one. - // this is different, but more internally consistent. - if (_prevDefinition == null || _nextDefinition == null) - return; + get => GetValue(PreviewContentProperty); + set => SetValue(PreviewContentProperty, value); + } - var delta = _orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y; - double max; - double min; - GetDeltaConstraints(out min, out max); - delta = Math.Min(Math.Max(delta, min), max); + /// + /// Converts BasedOnAlignment direction to Rows, Columns, or Both depending on its width/height. + /// + internal GridResizeDirection GetEffectiveResizeDirection() + { + GridResizeDirection direction = ResizeDirection; - var prevIsStar = IsStar(_prevDefinition); - var nextIsStar = IsStar(_nextDefinition); + if (direction != GridResizeDirection.Auto) + { + return direction; + } - if (prevIsStar && nextIsStar) + // When HorizontalAlignment is Left, Right or Center, resize Columns. + if (HorizontalAlignment != HorizontalAlignment.Stretch) + { + direction = GridResizeDirection.Columns; + } + else if (VerticalAlignment != VerticalAlignment.Stretch) { - foreach (var definition in _definitions) + direction = GridResizeDirection.Rows; + } + else if (Bounds.Width <= Bounds.Height) // Fall back to Width vs Height. + { + direction = GridResizeDirection.Columns; + } + else + { + direction = GridResizeDirection.Rows; + } + + return direction; + } + + /// + /// Convert BasedOnAlignment to Next/Prev/Both depending on alignment and Direction. + /// + private GridResizeBehavior GetEffectiveResizeBehavior(GridResizeDirection direction) + { + GridResizeBehavior resizeBehavior = ResizeBehavior; + + if (resizeBehavior == GridResizeBehavior.BasedOnAlignment) + { + if (direction == GridResizeDirection.Columns) { - if (definition == _prevDefinition) + switch (HorizontalAlignment) { - SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta); + case HorizontalAlignment.Left: + resizeBehavior = GridResizeBehavior.PreviousAndCurrent; + break; + case HorizontalAlignment.Right: + resizeBehavior = GridResizeBehavior.CurrentAndNext; + break; + default: + resizeBehavior = GridResizeBehavior.PreviousAndNext; + break; } - else if (definition == _nextDefinition) + } + else + { + switch (VerticalAlignment) { - SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta); + case VerticalAlignment.Top: + resizeBehavior = GridResizeBehavior.PreviousAndCurrent; + break; + case VerticalAlignment.Bottom: + resizeBehavior = GridResizeBehavior.CurrentAndNext; + break; + default: + resizeBehavior = GridResizeBehavior.PreviousAndNext; + break; } - else if (IsStar(definition)) + } + } + + return resizeBehavior; + } + + /// + /// Removes preview adorner from the grid. + /// + private void RemovePreviewAdorner() + { + if (_resizeData.Adorner != null) + { + AdornerLayer layer = AdornerLayer.GetAdornerLayer(this); + layer.Children.Remove(_resizeData.Adorner); + } + } + + /// + /// Initialize the data needed for resizing. + /// + private void InitializeData(bool showsPreview) + { + // If not in a grid or can't resize, do nothing. + if (Parent is Grid grid) + { + GridResizeDirection resizeDirection = GetEffectiveResizeDirection(); + + // Setup data used for resizing. + _resizeData = new ResizeData + { + Grid = grid, + ShowsPreview = showsPreview, + ResizeDirection = resizeDirection, + SplitterLength = Math.Min(Bounds.Width, Bounds.Height), + ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection) + }; + + // Store the rows and columns to resize on drag events. + if (!SetupDefinitionsToResize()) + { + // Unable to resize, clear data. + _resizeData = null; + return; + } + + // Setup the preview in the adorner if ShowsPreview is true. + SetupPreviewAdorner(); + } + } + + /// + /// Returns true if GridSplitter can resize rows/columns. + /// + private bool SetupDefinitionsToResize() + { + int gridSpan = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ? + Grid.ColumnSpanProperty : + Grid.RowSpanProperty); + + if (gridSpan == 1) + { + var splitterIndex = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ? + Grid.ColumnProperty : + Grid.RowProperty); + + // Select the columns based on behavior. + int index1, index2; + + switch (_resizeData.ResizeBehavior) + { + case GridResizeBehavior.PreviousAndCurrent: + // Get current and previous. + index1 = splitterIndex - 1; + index2 = splitterIndex; + break; + case GridResizeBehavior.CurrentAndNext: + // Get current and next. + index1 = splitterIndex; + index2 = splitterIndex + 1; + break; + default: // GridResizeBehavior.PreviousAndNext. + // Get previous and next. + index1 = splitterIndex - 1; + index2 = splitterIndex + 1; + break; + } + + // Get count of rows/columns in the resize direction. + int count = _resizeData.ResizeDirection == GridResizeDirection.Columns ? + _resizeData.Grid.ColumnDefinitions.Count : + _resizeData.Grid.RowDefinitions.Count; + + if (index1 >= 0 && index2 < count) + { + _resizeData.SplitterIndex = splitterIndex; + + _resizeData.Definition1Index = index1; + _resizeData.Definition1 = GetGridDefinition(_resizeData.Grid, index1, _resizeData.ResizeDirection); + _resizeData.OriginalDefinition1Length = + _resizeData.Definition1.UserSizeValueCache; // Save Size if user cancels. + _resizeData.OriginalDefinition1ActualLength = GetActualLength(_resizeData.Definition1); + + _resizeData.Definition2Index = index2; + _resizeData.Definition2 = GetGridDefinition(_resizeData.Grid, index2, _resizeData.ResizeDirection); + _resizeData.OriginalDefinition2Length = + _resizeData.Definition2.UserSizeValueCache; // Save Size if user cancels. + _resizeData.OriginalDefinition2ActualLength = GetActualLength(_resizeData.Definition2); + + // Determine how to resize the columns. + bool isStar1 = IsStar(_resizeData.Definition1); + bool isStar2 = IsStar(_resizeData.Definition2); + + if (isStar1 && isStar2) { - SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars. + // If they are both stars, resize both. + _resizeData.SplitBehavior = SplitBehavior.Split; } + else + { + // One column is fixed width, resize the first one that is fixed. + _resizeData.SplitBehavior = !isStar1 ? SplitBehavior.Resize1 : SplitBehavior.Resize2; + } + + return true; } } - else if (prevIsStar) + + return false; + } + + /// + /// Create the preview adorner and add it to the adorner layer. + /// + private void SetupPreviewAdorner() + { + if (_resizeData.ShowsPreview) { - SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta); + // Get the adorner layer and add an adorner to it. + var adornerLayer = AdornerLayer.GetAdornerLayer(_resizeData.Grid); + + var previewContent = PreviewContent; + + // Can't display preview. + if (adornerLayer == null) + { + return; + } + + IControl builtPreviewContent = previewContent?.Build(); + + _resizeData.Adorner = new PreviewAdorner(builtPreviewContent); + + AdornerLayer.SetAdornedElement(_resizeData.Adorner, this); + + adornerLayer.Children.Add(_resizeData.Adorner); + + // Get constraints on preview's translation. + GetDeltaConstraints(out _resizeData.MinChange, out _resizeData.MaxChange); } - else if (nextIsStar) + } + + protected override void OnPointerEnter(PointerEventArgs e) + { + base.OnPointerEnter(e); + + GridResizeDirection direction = GetEffectiveResizeDirection(); + + switch (direction) { - SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta); + case GridResizeDirection.Columns: + Cursor = s_columnSplitterCursor; + break; + case GridResizeDirection.Rows: + Cursor = s_rowSplitterCursor; + break; } - else + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + + if (_resizeData != null) { - SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta); - SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta); + CancelResize(); } } - private double GetActualLength(DefinitionBase definition) + protected override void OnDragStarted(VectorEventArgs e) { - if (definition == null) - return 0; - var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.ActualWidth ?? ((RowDefinition)definition).ActualHeight; + base.OnDragStarted(e); + + Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called"); + + InitializeData(ShowsPreview); } - private double GetMinLength(DefinitionBase definition) + protected override void OnDragDelta(VectorEventArgs e) { - if (definition == null) - return 0; - var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.MinWidth ?? ((RowDefinition)definition).MinHeight; + base.OnDragDelta(e); + + if (_resizeData != null) + { + double horizontalChange = e.Vector.X; + double verticalChange = e.Vector.Y; + + // Round change to nearest multiple of DragIncrement. + double dragIncrement = DragIncrement; + horizontalChange = Math.Round(horizontalChange / dragIncrement) * dragIncrement; + verticalChange = Math.Round(verticalChange / dragIncrement) * dragIncrement; + + if (_resizeData.ShowsPreview) + { + // Set the Translation of the Adorner to the distance from the thumb. + if (_resizeData.ResizeDirection == GridResizeDirection.Columns) + { + _resizeData.Adorner.OffsetX = Math.Min( + Math.Max(horizontalChange, _resizeData.MinChange), + _resizeData.MaxChange); + } + else + { + _resizeData.Adorner.OffsetY = Math.Min( + Math.Max(verticalChange, _resizeData.MinChange), + _resizeData.MaxChange); + } + } + else + { + // Directly update the grid. + MoveSplitter(horizontalChange, verticalChange); + } + } } - private double GetMaxLength(DefinitionBase definition) + protected override void OnDragCompleted(VectorEventArgs e) { - if (definition == null) - return 0; - var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.MaxWidth ?? ((RowDefinition)definition).MaxHeight; + base.OnDragCompleted(e); + + if (_resizeData != null) + { + if (_resizeData.ShowsPreview) + { + // Update the grid. + MoveSplitter(_resizeData.Adorner.OffsetX, _resizeData.Adorner.OffsetY); + RemovePreviewAdorner(); + } + + _resizeData = null; + } } - private bool IsStar(DefinitionBase definition) + protected override void OnKeyDown(KeyEventArgs e) { - var columnDefinition = definition as ColumnDefinition; - return columnDefinition?.Width.IsStar ?? ((RowDefinition)definition).Height.IsStar; + Key key = e.Key; + + switch (key) + { + case Key.Escape: + if (_resizeData != null) + { + CancelResize(); + e.Handled = true; + } + + break; + + case Key.Left: + e.Handled = KeyboardMoveSplitter(-KeyboardIncrement, 0); + break; + case Key.Right: + e.Handled = KeyboardMoveSplitter(KeyboardIncrement, 0); + break; + case Key.Up: + e.Handled = KeyboardMoveSplitter(0, -KeyboardIncrement); + break; + case Key.Down: + e.Handled = KeyboardMoveSplitter(0, KeyboardIncrement); + break; + } } - private void SetLengthInStars(DefinitionBase definition, double value) + /// + /// Cancels the resize operation. + /// + private void CancelResize() { - var columnDefinition = definition as ColumnDefinition; - if (columnDefinition != null) + // Restore original column/row lengths. + if (_resizeData.ShowsPreview) { - columnDefinition.Width = new GridLength(value, GridUnitType.Star); + RemovePreviewAdorner(); } - else + else // Reset the columns/rows lengths to the saved values. { - ((RowDefinition)definition).Height = new GridLength(value, GridUnitType.Star); + SetDefinitionLength(_resizeData.Definition1, _resizeData.OriginalDefinition1Length); + SetDefinitionLength(_resizeData.Definition2, _resizeData.OriginalDefinition2Length); } + + _resizeData = null; } - private void SetLength(DefinitionBase definition, double value) + /// + /// Returns true if the row/column has a star length. + /// + private static bool IsStar(DefinitionBase definition) { - var columnDefinition = definition as ColumnDefinition; - if (columnDefinition != null) + return definition.UserSizeValueCache.IsStar; + } + + /// + /// Gets Column or Row definition at index from grid based on resize direction. + /// + private static DefinitionBase GetGridDefinition(Grid grid, int index, GridResizeDirection direction) + { + return direction == GridResizeDirection.Columns ? + (DefinitionBase)grid.ColumnDefinitions[index] : + (DefinitionBase)grid.RowDefinitions[index]; + } + + /// + /// Retrieves the ActualWidth or ActualHeight of the definition depending on its type Column or Row. + /// + private double GetActualLength(DefinitionBase definition) + { + var column = definition as ColumnDefinition; + + return column?.ActualWidth ?? ((RowDefinition)definition).ActualHeight; + } + + /// + /// Gets Column or Row definition at index from grid based on resize direction. + /// + private static void SetDefinitionLength(DefinitionBase definition, GridLength length) + { + definition.SetValue( + definition is ColumnDefinition ? ColumnDefinition.WidthProperty : RowDefinition.HeightProperty, length); + } + + /// + /// Get the minimum and maximum Delta can be given definition constraints (MinWidth/MaxWidth). + /// + private void GetDeltaConstraints(out double minDelta, out double maxDelta) + { + double definition1Len = GetActualLength(_resizeData.Definition1); + double definition1Min = _resizeData.Definition1.UserMinSizeValueCache; + double definition1Max = _resizeData.Definition1.UserMaxSizeValueCache; + + double definition2Len = GetActualLength(_resizeData.Definition2); + double definition2Min = _resizeData.Definition2.UserMinSizeValueCache; + double definition2Max = _resizeData.Definition2.UserMaxSizeValueCache; + + // Set MinWidths to be greater than width of splitter. + if (_resizeData.SplitterIndex == _resizeData.Definition1Index) + { + definition1Min = Math.Max(definition1Min, _resizeData.SplitterLength); + } + else if (_resizeData.SplitterIndex == _resizeData.Definition2Index) + { + definition2Min = Math.Max(definition2Min, _resizeData.SplitterLength); + } + + if (_resizeData.SplitBehavior == SplitBehavior.Split) { - columnDefinition.Width = new GridLength(value); + // Determine the minimum and maximum the columns can be resized. + minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len); + maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min); + } + else if (_resizeData.SplitBehavior == SplitBehavior.Resize1) + { + minDelta = definition1Min - definition1Len; + maxDelta = definition1Max - definition1Len; } else { - ((RowDefinition)definition).Height = new GridLength(value); + minDelta = definition2Len - definition2Max; + maxDelta = definition2Len - definition2Min; } } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + /// + /// Sets the length of definition1 and definition2. + /// + private void SetLengths(double definition1Pixels, double definition2Pixels) { - base.OnAttachedToVisualTree(e); - _grid = this.GetVisualParent(); + // For the case where both definition1 and 2 are stars, update all star values to match their current pixel values. + if (_resizeData.SplitBehavior == SplitBehavior.Split) + { + var definitions = _resizeData.ResizeDirection == GridResizeDirection.Columns ? + (IAvaloniaReadOnlyList)_resizeData.Grid.ColumnDefinitions : + (IAvaloniaReadOnlyList)_resizeData.Grid.RowDefinitions; - _orientation = DetectOrientation(); + var definitionsCount = definitions.Count; - int definitionIndex; //row or col - if (_orientation == Orientation.Vertical) + for (var i = 0; i < definitionsCount; i++) + { + DefinitionBase definition = definitions[i]; + + // For each definition, if it is a star, set is value to ActualLength in stars + // This makes 1 star == 1 pixel in length + if (i == _resizeData.Definition1Index) + { + SetDefinitionLength(definition, new GridLength(definition1Pixels, GridUnitType.Star)); + } + else if (i == _resizeData.Definition2Index) + { + SetDefinitionLength(definition, new GridLength(definition2Pixels, GridUnitType.Star)); + } + else if (IsStar(definition)) + { + SetDefinitionLength(definition, new GridLength(GetActualLength(definition), GridUnitType.Star)); + } + } + } + else if (_resizeData.SplitBehavior == SplitBehavior.Resize1) { - Cursor = new Cursor(StandardCursorType.SizeWestEast); - _definitions = _grid.ColumnDefinitions.Cast().ToList(); - definitionIndex = GetValue(Grid.ColumnProperty); - PseudoClasses.Add(":vertical"); + SetDefinitionLength(_resizeData.Definition1, new GridLength(definition1Pixels)); } else { - Cursor = new Cursor(StandardCursorType.SizeNorthSouth); - definitionIndex = GetValue(Grid.RowProperty); - _definitions = _grid.RowDefinitions.Cast().ToList(); - PseudoClasses.Add(":horizontal"); + SetDefinitionLength(_resizeData.Definition2, new GridLength(definition2Pixels)); } + } - if (definitionIndex > 0) - _prevDefinition = _definitions[definitionIndex - 1]; + /// + /// Move the splitter by the given Delta's in the horizontal and vertical directions. + /// + private void MoveSplitter(double horizontalChange, double verticalChange) + { + Debug.Assert(_resizeData != null, "_resizeData should not be null when calling MoveSplitter"); + + // Calculate the offset to adjust the splitter. + var delta = _resizeData.ResizeDirection == GridResizeDirection.Columns ? horizontalChange : verticalChange; + + DefinitionBase definition1 = _resizeData.Definition1; + DefinitionBase definition2 = _resizeData.Definition2; + + if (definition1 != null && definition2 != null) + { + double actualLength1 = GetActualLength(definition1); + double actualLength2 = GetActualLength(definition2); + + // When splitting, Check to see if the total pixels spanned by the definitions + // is the same asbefore starting resize. If not cancel the drag + if (_resizeData.SplitBehavior == SplitBehavior.Split && + !MathUtilities.AreClose( + actualLength1 + actualLength2, + _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength)) + { + CancelResize(); - if (definitionIndex < _definitions.Count - 1) - _nextDefinition = _definitions[definitionIndex + 1]; + return; + } + + GetDeltaConstraints(out var min, out var max); + + // Constrain Delta to Min/MaxWidth of columns + delta = Math.Min(Math.Max(delta, min), max); + + double definition1LengthNew = actualLength1 + delta; + double definition2LengthNew = actualLength1 + actualLength2 - definition1LengthNew; + + SetLengths(definition1LengthNew, definition2LengthNew); + } } - private Orientation DetectOrientation() + /// + /// Move the splitter using the Keyboard (Don't show preview). + /// + private bool KeyboardMoveSplitter(double horizontalChange, double verticalChange) { - if (!_grid.ColumnDefinitions.Any()) - return Orientation.Horizontal; - if (!_grid.RowDefinitions.Any()) - return Orientation.Vertical; + // If moving with the mouse, ignore keyboard motion. + if (_resizeData != null) + { + return false; // Don't handle the event. + } - var col = GetValue(Grid.ColumnProperty); - var row = GetValue(Grid.RowProperty); - var width = _grid.ColumnDefinitions[col].Width; - var height = _grid.RowDefinitions[row].Height; - if (width.IsAuto && !height.IsAuto) + // Don't show preview. + InitializeData(false); + + // Check that we are actually able to resize. + if (_resizeData == null) { - return Orientation.Vertical; + return false; // Don't handle the event. } - if (!width.IsAuto && height.IsAuto) + + MoveSplitter(horizontalChange, verticalChange); + + _resizeData = null; + + return true; + } + + /// + /// This adorner draws the preview for the . + /// It also positions the adorner. + /// + private sealed class PreviewAdorner : Decorator + { + private readonly TranslateTransform _translation; + private readonly Decorator _decorator; + + public PreviewAdorner(IControl previewControl) { - return Orientation.Horizontal; + // Add a decorator to perform translations. + _translation = new TranslateTransform(); + + _decorator = new Decorator + { + Child = previewControl, + RenderTransform = _translation + }; + + Child = _decorator; + } + + /// + /// The Preview's Offset in the X direction from the GridSplitter. + /// + public double OffsetX + { + get => _translation.X; + set => _translation.X = value; + } + + /// + /// The Preview's Offset in the Y direction from the GridSplitter. + /// + public double OffsetY + { + get => _translation.Y; + set => _translation.Y = value; } - if (_grid.Children.OfType() // Decision based on other controls in the same column - .Where(c => Grid.GetColumn(c) == col) - .Any(c => c.GetType() != typeof(GridSplitter))) + + protected override Size ArrangeOverride(Size finalSize) { - return Orientation.Horizontal; + // Adorners always get clipped to the owner control. In this case we want + // to constrain size to the splitter size but draw on top of the parent grid. + Clip = null; + + return base.ArrangeOverride(finalSize); } - return Orientation.Vertical; + } + + /// + /// has special Behavior when columns are fixed. + /// If the left column is fixed, splitter will only resize that column. + /// Else if the right column is fixed, splitter will only resize the right column. + /// + private enum SplitBehavior + { + /// + /// Both columns/rows are star lengths. + /// + Split, + + /// + /// Resize 1 only. + /// + Resize1, + + /// + /// Resize 2 only. + /// + Resize2 + } + + /// + /// Stores data during the resizing operation. + /// + private class ResizeData + { + public bool ShowsPreview; + public PreviewAdorner Adorner; + + // The constraints to keep the Preview within valid ranges. + public double MinChange; + public double MaxChange; + + // The grid to Resize. + public Grid Grid; + + // Cache of Resize Direction and Behavior. + public GridResizeDirection ResizeDirection; + public GridResizeBehavior ResizeBehavior; + + // The columns/rows to resize. + public DefinitionBase Definition1; + public DefinitionBase Definition2; + + // Are the columns/rows star lengths. + public SplitBehavior SplitBehavior; + + // The index of the splitter. + public int SplitterIndex; + + // The indices of the columns/rows. + public int Definition1Index; + public int Definition2Index; + + // The original lengths of Definition1 and Definition2 (to restore lengths if user cancels resize). + public GridLength OriginalDefinition1Length; + public GridLength OriginalDefinition2Length; + public double OriginalDefinition1ActualLength; + public double OriginalDefinition2ActualLength; + + // The minimum of Width/Height of Splitter. Used to ensure splitter + // isn't hidden by resizing a row/column smaller than the splitter. + public double SplitterLength; } } } diff --git a/src/Avalonia.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/GridSplitter.xaml index 64349222ea..cfab5dab56 100644 --- a/src/Avalonia.Themes.Default/GridSplitter.xaml +++ b/src/Avalonia.Themes.Default/GridSplitter.xaml @@ -1,51 +1,21 @@ - - - - - - - - + + - - - - - - - + - + diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index a790d2fca1..15d62e9140 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -2,9 +2,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Platform; using Avalonia.UnitTests; - using Moq; - using Xunit; namespace Avalonia.Controls.UnitTests @@ -21,183 +19,246 @@ namespace Avalonia.Controls.UnitTests public void Detects_Horizontal_Orientation() { GridSplitter splitter; - var grid = new Grid() - { - RowDefinitions = new RowDefinitions("*,Auto,*"), - ColumnDefinitions = new ColumnDefinitions("*,*"), - Children = - { - new Border { [Grid.RowProperty] = 0 }, - (splitter = new GridSplitter { [Grid.RowProperty] = 1 }), - new Border { [Grid.RowProperty] = 2 } - } - }; + + var grid = new Grid + { + RowDefinitions = new RowDefinitions("*,Auto,*"), + ColumnDefinitions = new ColumnDefinitions("*,*"), + Children = + { + new Border { [Grid.RowProperty] = 0 }, + (splitter = new GridSplitter { [Grid.RowProperty] = 1 }), + new Border { [Grid.RowProperty] = 2 } + } + }; var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(splitter.Classes, ":horizontal".Equals); + Assert.Equal(GridResizeDirection.Rows, splitter.GetEffectiveResizeDirection()); } [Fact] public void Detects_Vertical_Orientation() { GridSplitter splitter; - var grid = new Grid() - { - ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), - RowDefinitions = new RowDefinitions("*,*"), - Children = - { - new Border { [Grid.ColumnProperty] = 0 }, - (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), - new Border { [Grid.ColumnProperty] = 2 }, - } - }; + + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), + RowDefinitions = new RowDefinitions("*,*"), + Children = + { + new Border { [Grid.ColumnProperty] = 0 }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }), + new Border { [Grid.ColumnProperty] = 2 }, + } + }; var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(splitter.Classes, ":vertical".Equals); + Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection()); } [Fact] public void Detects_With_Both_Auto() { GridSplitter splitter; - var grid = new Grid() - { - ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), - RowDefinitions = new RowDefinitions("Auto,Auto"), - Children = - { - new Border { [Grid.ColumnProperty] = 0 }, - (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), - new Border { [Grid.ColumnProperty] = 2 }, - } - }; + + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), + RowDefinitions = new RowDefinitions("Auto,Auto"), + Children = + { + new Border { [Grid.ColumnProperty] = 0 }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }), + new Border { [Grid.ColumnProperty] = 2 }, + } + }; var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(splitter.Classes, ":vertical".Equals); + Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection()); } [Fact] - public void Horizontal_Stays_Within_Constraints() + public void In_First_Position_Doesnt_Throw_Exception() + { + GridSplitter splitter; + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), + RowDefinitions = new RowDefinitions("*,*"), + Children = + { + (splitter = new GridSplitter { [Grid.ColumnProperty] = 0 }), + new Border { [Grid.ColumnProperty] = 1 }, + new Border { [Grid.ColumnProperty] = 2 }, + } + }; + + var root = new TestRoot { Child = grid }; + root.Measure(new Size(100, 300)); + root.Arrange(new Rect(0, 0, 100, 300)); + + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, Vector = new Vector(100, 1000) + }); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Horizontal_Stays_Within_Constraints(bool showsPreview) { var control1 = new Border { [Grid.RowProperty] = 0 }; - var splitter = new GridSplitter - { - [Grid.RowProperty] = 1, - }; + var splitter = new GridSplitter { [Grid.RowProperty] = 1, ShowsPreview = showsPreview}; var control2 = new Border { [Grid.RowProperty] = 2 }; - var rowDefinitions = new RowDefinitions() - { - new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 }, - new RowDefinition(GridLength.Auto), - new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 }, - }; - - var grid = new Grid() - { - RowDefinitions = rowDefinitions, - Children = - { - control1, splitter, control2 - } - }; + var rowDefinitions = new RowDefinitions + { + new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 }, + new RowDefinition(GridLength.Auto), + new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 }, + }; + + var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = new VisualLayerManager + { + Child = grid + } + }; - var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 200)); root.Arrange(new Rect(0, 0, 100, 200)); + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(0, -100) - }); - Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star)); - Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star)); + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(0, -100) + }); + + if (showsPreview) + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star)); + } + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(0, 100) - }); - Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star)); - Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star)); - } + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(0, 100) + }); - [Fact] - public void In_First_Position_Doesnt_Throw_Exception() - { - GridSplitter splitter; - var grid = new Grid() - { - ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), - RowDefinitions = new RowDefinitions("*,*"), - Children = - { - (splitter = new GridSplitter { [Grid.ColumnProperty] = 0} ), - new Border { [Grid.ColumnProperty] = 1 }, - new Border { [Grid.ColumnProperty] = 2 }, - } - }; + if (showsPreview) + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star)); + } - var root = new TestRoot { Child = grid }; - root.Measure(new Size(100, 300)); - root.Arrange(new Rect(0, 0, 100, 300)); splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(100, 1000) - }); + { + RoutedEvent = Thumb.DragCompletedEvent + }); + + Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star)); } - [Fact] - public void Vertical_Stays_Within_Constraints() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Vertical_Stays_Within_Constraints(bool showsPreview) { var control1 = new Border { [Grid.ColumnProperty] = 0 }; - var splitter = new GridSplitter - { - [Grid.ColumnProperty] = 1, - }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, ShowsPreview = showsPreview}; var control2 = new Border { [Grid.ColumnProperty] = 2 }; - var columnDefinitions = new ColumnDefinitions() - { - new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 }, - new ColumnDefinition(GridLength.Auto), - new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 }, - }; - - var grid = new Grid() - { - ColumnDefinitions = columnDefinitions, - Children = - { - control1, splitter, control2 - } - }; + var columnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 }, + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 }, + }; - var root = new TestRoot { Child = grid }; + var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = new VisualLayerManager + { + Child = grid + } + }; root.Measure(new Size(200, 100)); root.Arrange(new Rect(0, 0, 200, 100)); + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(-100, 0) - }); - Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star)); - Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star)); + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(-100, 0) + }); + + if (showsPreview) + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star)); + } + splitter.RaiseEvent(new VectorEventArgs - { - RoutedEvent = Thumb.DragDeltaEvent, - Vector = new Vector(100, 0) - }); + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(100, 0) + }); + + if (showsPreview) + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); + } + else + { + Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); + } + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragCompletedEvent + }); + Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); } From ab26c18a8f882b1665cbd62dfdbc5a877c5c534e Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sat, 26 Oct 2019 21:01:43 +0300 Subject: [PATCH 024/126] typo fix --- .../Avalonia.Controls.UnitTests/GridTests.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index b74dd641df..a2d6f14b26 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1383,7 +1383,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { if (setUsingAvaloniaProperty) grid.ColumnDefinitions[0][ColumnDefinition.WidthProperty] = new GridLength(5); @@ -1399,7 +1399,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { if (setUsingAvaloniaProperty) grid.ColumnDefinitions[0][ColumnDefinition.MinWidthProperty] = 5; @@ -1415,7 +1415,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { if (setUsingAvaloniaProperty) grid.ColumnDefinitions[0][ColumnDefinition.MaxWidthProperty] = 5; @@ -1431,7 +1431,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { if (setUsingAvaloniaProperty) grid.RowDefinitions[0][RowDefinition.HeightProperty] = new GridLength(5); @@ -1447,7 +1447,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { if (setUsingAvaloniaProperty) grid.RowDefinitions[0][RowDefinition.MinHeightProperty] = 5; @@ -1463,7 +1463,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { if (setUsingAvaloniaProperty) grid.RowDefinitions[0][RowDefinition.MaxHeightProperty] = 5; @@ -1477,7 +1477,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { grid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(5))); }); @@ -1488,7 +1488,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { grid.RowDefinitions.Add(new RowDefinition(new GridLength(5))); }); @@ -1499,7 +1499,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { grid.ColumnDefinitions = ColumnDefinitions.Parse("2*,1*"); }); @@ -1510,7 +1510,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { grid.RowDefinitions = RowDefinitions.Parse("2*,1*"); }); @@ -1521,7 +1521,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { grid.ColumnDefinitions.RemoveAt(0); }); @@ -1532,7 +1532,7 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") }; - Change_Propery_And_Verify_Measure_Requested(grid, () => + Change_Property_And_Verify_Measure_Requested(grid, () => { grid.RowDefinitions.RemoveAt(0); }); @@ -1601,7 +1601,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(10, grid.Children[1].Bounds.Width); } - private static void Change_Propery_And_Verify_Measure_Requested(Grid grid, Action change) + private static void Change_Property_And_Verify_Measure_Requested(Grid grid, Action change) { grid.Measure(new Size(100, 100)); grid.Arrange(new Rect(grid.DesiredSize)); From bf04d22856d36ffeded87fedd211de21c36a66f3 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 19:49:20 +0100 Subject: [PATCH 025/126] Comment fixes. --- src/Avalonia.Controls/GridSplitter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index e5749c8ed9..3fc1ed3ea5 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -16,7 +16,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// Enum to indicate whether GridSplitter resizes Columns or Rows. + /// Enum to indicate whether resizes Columns or Rows. /// public enum GridResizeDirection { @@ -38,7 +38,7 @@ namespace Avalonia.Controls } /// - /// Enum to indicate what Columns or Rows the GridSplitter resizes. + /// Enum to indicate what Columns or Rows the resizes. /// public enum GridResizeBehavior { From 14b3683127db6661c127a98b79b7842d2d44d894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 26 Oct 2019 20:47:10 +0100 Subject: [PATCH 026/126] Replaced obsolete API usages. --- src/Avalonia.Controls/ListBox.cs | 20 ++++++++++++------- .../NumericUpDown/NumericUpDown.cs | 4 ++-- src/Avalonia.Controls/Primitives/TabStrip.cs | 10 ++++++++-- src/Avalonia.Controls/Primitives/Thumb.cs | 2 -- src/Avalonia.Controls/TextBox.cs | 8 ++++---- src/Avalonia.Controls/TreeView.cs | 19 +++++++++++------- src/Avalonia.Input/AccessKeyHandler.cs | 2 +- src/Avalonia.Input/FocusManager.cs | 2 +- 8 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 449ca18465..b15a6ce668 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -132,14 +133,19 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) + if (e.Source is IVisual source) { - e.Handled = UpdateSelectionFromEventSource( - e.Source, - true, - (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0, - e.MouseButton == MouseButton.Right); + var point = e.GetCurrentPoint(source); + + if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed) + { + e.Handled = UpdateSelectionFromEventSource( + e.Source, + true, + (e.KeyModifiers & KeyModifiers.Shift) != 0, + (e.KeyModifiers & KeyModifiers.Control) != 0, + point.Properties.IsRightButtonPressed); + } } } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index f8ae5c9690..6d450a0155 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -804,9 +804,9 @@ namespace Avalonia.Controls private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e) { - if (e.Device.Captured != Spinner) + if (e.Pointer.Captured != Spinner) { - Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input); + Dispatcher.UIThread.InvokeAsync(() => { e.Pointer.Capture(Spinner); }, DispatcherPriority.Input); } } diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index ec0dbd124c..e1a6cf79bb 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -44,9 +45,14 @@ namespace Avalonia.Controls.Primitives { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left) + if (e.Source is IVisual source) { - e.Handled = UpdateSelectionFromEventSource(e.Source); + var point = e.GetCurrentPoint(source); + + if (point.Properties.IsLeftButtonPressed) + { + e.Handled = UpdateSelectionFromEventSource(e.Source); + } } } } diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 9f5ddb666c..d88018cf32 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -73,7 +73,6 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerPressed(PointerPressedEventArgs e) { - e.Device.Capture(this); e.Handled = true; _lastPoint = e.GetPosition(this); @@ -92,7 +91,6 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { - e.Device.Capture(null); e.Handled = true; _lastPoint = null; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index fdc9d153e2..3d472fca18 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -677,13 +677,13 @@ namespace Avalonia.Controls } } - e.Device.Capture(_presenter); + e.Pointer.Capture(_presenter); e.Handled = true; } protected override void OnPointerMoved(PointerEventArgs e) { - if (_presenter != null && e.Device.Captured == _presenter) + if (_presenter != null && e.Pointer.Captured == _presenter) { var point = e.GetPosition(_presenter); @@ -694,9 +694,9 @@ namespace Avalonia.Controls protected override void OnPointerReleased(PointerReleasedEventArgs e) { - if (_presenter != null && e.Device.Captured == _presenter) + if (_presenter != null && e.Pointer.Captured == _presenter) { - e.Device.Capture(null); + e.Pointer.Capture(null); } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 8907137ecb..59844be8a6 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -507,14 +507,19 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right) + if (e.Source is IVisual source) { - e.Handled = UpdateSelectionFromEventSource( - e.Source, - true, - (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0, - e.MouseButton == MouseButton.Right); + var point = e.GetCurrentPoint(source); + + if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed) + { + e.Handled = UpdateSelectionFromEventSource( + e.Source, + true, + (e.KeyModifiers & KeyModifiers.Shift) != 0, + (e.KeyModifiers & KeyModifiers.Control) != 0, + point.Properties.IsRightButtonPressed); + } } } diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 9e4b2b84e0..aa009770f6 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -182,7 +182,7 @@ namespace Avalonia.Input { bool menuIsOpen = MainMenu?.IsOpen == true; - if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen) + if ((e.KeyModifiers & KeyModifiers.Alt) != 0 || menuIsOpen) { // If any other key is pressed with the Alt key held down, or the main menu is open, // find all controls who have registered that access key. diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 80e18cb7bf..104ac9cb61 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -180,7 +180,7 @@ namespace Avalonia.Input if (sender == e.Source && ev.MouseButton == MouseButton.Left) { - var element = (ev.Device?.Captured as IInputElement) ?? (e.Source as IInputElement); + var element = (ev.Pointer?.Captured as IInputElement) ?? (e.Source as IInputElement); if (element == null || !CanFocus(element)) { From b315d5024f28d82f8eb51b0a46a86085d13d24d9 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 21:42:59 +0100 Subject: [PATCH 027/126] Move enums lower. Add workaround for thumb raising multiple started events. --- src/Avalonia.Controls/GridSplitter.cs | 106 ++++++++++++++------------ 1 file changed, 56 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 3fc1ed3ea5..56a28e15c2 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -16,55 +16,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// Enum to indicate whether resizes Columns or Rows. - /// - public enum GridResizeDirection - { - /// - /// Determines whether to resize rows or columns based on its Alignment and - /// width compared to height. - /// - Auto, - - /// - /// Resize columns when dragging Splitter. - /// - Columns, - - /// - /// Resize rows when dragging Splitter. - /// - Rows - } - - /// - /// Enum to indicate what Columns or Rows the resizes. - /// - public enum GridResizeBehavior - { - /// - /// Determine which columns or rows to resize based on its Alignment. - /// - BasedOnAlignment, - - /// - /// Resize the current and next Columns or Rows. - /// - CurrentAndNext, - - /// - /// Resize the previous and current Columns or Rows. - /// - PreviousAndCurrent, - - /// - /// Resize the previous and next Columns or Rows. - /// - PreviousAndNext - } - - /// - /// Represents the control that redistributes space between columns or rows of a Grid control. + /// Represents the control that redistributes space between columns or rows of a control. /// public class GridSplitter : Thumb { @@ -427,7 +379,13 @@ namespace Avalonia.Controls { base.OnDragStarted(e); - Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called"); + // TODO: Looks like that sometimes thumb will raise multiple drag started events. + // Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called"); + + if (_resizeData != null) + { + return; + } InitializeData(ShowsPreview); } @@ -845,4 +803,52 @@ namespace Avalonia.Controls public double SplitterLength; } } + + /// + /// Enum to indicate whether resizes Columns or Rows. + /// + public enum GridResizeDirection + { + /// + /// Determines whether to resize rows or columns based on its Alignment and + /// width compared to height. + /// + Auto, + + /// + /// Resize columns when dragging Splitter. + /// + Columns, + + /// + /// Resize rows when dragging Splitter. + /// + Rows + } + + /// + /// Enum to indicate what Columns or Rows the resizes. + /// + public enum GridResizeBehavior + { + /// + /// Determine which columns or rows to resize based on its Alignment. + /// + BasedOnAlignment, + + /// + /// Resize the current and next Columns or Rows. + /// + CurrentAndNext, + + /// + /// Resize the previous and current Columns or Rows. + /// + PreviousAndCurrent, + + /// + /// Resize the previous and next Columns or Rows. + /// + PreviousAndNext + } } From 9c5e688958c2f2590644d8e54f1951d92d8551c6 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 22:13:49 +0100 Subject: [PATCH 028/126] Fix Grid shared scope not being updated when resizing row/column definitions. --- src/Avalonia.Controls/ColumnDefinition.cs | 4 ++- src/Avalonia.Controls/DefinitionBase.cs | 33 +++++++++++++++-- src/Avalonia.Controls/RowDefinition.cs | 4 ++- .../Avalonia.Controls.UnitTests/GridTests.cs | 35 +++++++++++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index 6cad357e93..293b6326d6 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -31,7 +31,9 @@ namespace Avalonia.Controls /// static ColumnDefinition() { - AffectsParentMeasure(WidthProperty, MinWidthProperty, MaxWidthProperty); + AffectsParentMeasure(MinWidthProperty, MaxWidthProperty); + + WidthProperty.Changed.AddClassHandler(OnUserSizePropertyChanged); } /// diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 6ad32f080a..0b45d7e16a 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -7,9 +7,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; - -using Avalonia; -using Avalonia.Collections; using Avalonia.Utilities; namespace Avalonia.Controls @@ -118,6 +115,36 @@ namespace Avalonia.Controls } } + /// + /// This method needs to be internal to be accessable from derived classes. + /// + internal static void OnUserSizePropertyChanged(DefinitionBase definition, AvaloniaPropertyChangedEventArgs e) + { + if (definition.Parent == null) + { + return; + } + + if (definition._sharedState != null) + { + definition._sharedState.Invalidate(); + } + else + { + GridUnitType oldUnitType = ((GridLength)e.OldValue).GridUnitType; + GridUnitType newUnitType = ((GridLength)e.NewValue).GridUnitType; + + if (oldUnitType != newUnitType) + { + definition.Parent.Invalidate(); + } + else + { + definition.Parent.InvalidateMeasure(); + } + } + } + /// /// Returns true if this definition is a part of shared group. /// diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs index 1a1a7e770b..85e7ed6519 100644 --- a/src/Avalonia.Controls/RowDefinition.cs +++ b/src/Avalonia.Controls/RowDefinition.cs @@ -31,7 +31,9 @@ namespace Avalonia.Controls /// static RowDefinition() { - AffectsParentMeasure(HeightProperty, MaxHeightProperty, MinHeightProperty); + AffectsParentMeasure(MaxHeightProperty, MinHeightProperty); + + HeightProperty.Changed.AddClassHandler(OnUserSizePropertyChanged); } /// diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index a2d6f14b26..353bb9c98d 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1172,6 +1172,41 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); } + [Fact] + public void Size_Group_Definition_Resizes_Are_Tracked() + { + var grids = new[] { + CreateGrid(("A", new GridLength(5, GridUnitType.Pixel)), (null, new GridLength())), + CreateGrid(("A", new GridLength(5, GridUnitType.Pixel)), (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(5, grids[0].ColumnDefinitions[0].ActualWidth); + Assert.Equal(5, grids[1].ColumnDefinitions[0].ActualWidth); + + grids[0].ColumnDefinitions[0].Width = new GridLength(10, GridUnitType.Pixel); + + foreach (Grid grid in grids) + { + grid.Measure(new Size(50, 50)); + grid.Arrange(new Rect(new Point(), new Point(50, 50))); + } + + PrintColumnDefinitions(grids[0]); + Assert.Equal(10, grids[0].ColumnDefinitions[0].ActualWidth); + Assert.Equal(10, grids[1].ColumnDefinitions[0].ActualWidth); + } + [Fact] public void Collection_Changes_Are_Tracked() { From 969166422eff8622f2f48963cf169f9f39b40c7d Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 22:23:38 +0100 Subject: [PATCH 029/126] Comment fix. --- src/Avalonia.Controls/DefinitionBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 0b45d7e16a..e4ae777453 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -116,7 +116,7 @@ namespace Avalonia.Controls } /// - /// This method needs to be internal to be accessable from derived classes. + /// Notifies parent or size scope that definition size has been changed. /// internal static void OnUserSizePropertyChanged(DefinitionBase definition, AvaloniaPropertyChangedEventArgs e) { From 1d7feade1b6b8dffbb83487a0f0c588988200467 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 23:32:20 +0100 Subject: [PATCH 030/126] Add keyboard input tests. --- .../GridSplitterTests.cs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index 15d62e9140..f2b6b0db4b 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -262,5 +262,123 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star)); Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); } + + [Theory] + [InlineData(Key.Up, 90, 110)] + [InlineData(Key.Down, 110, 90)] + public void Vertical_Keyboard_Input_Can_Move_Splitter(Key key, double expectedHeightFirst, double expectedHeightSecond) + { + var control1 = new Border { [Grid.RowProperty] = 0 }; + var splitter = new GridSplitter { [Grid.RowProperty] = 1, KeyboardIncrement = 10d }; + var control2 = new Border { [Grid.RowProperty] = 2 }; + + var rowDefinitions = new RowDefinitions + { + new RowDefinition(1, GridUnitType.Star), + new RowDefinition(GridLength.Auto), + new RowDefinition(1, GridUnitType.Star) + }; + + var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = grid + }; + + root.Measure(new Size(200, 200)); + root.Arrange(new Rect(0, 0, 200, 200)); + + splitter.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = key + }); + + Assert.Equal(rowDefinitions[0].Height, new GridLength(expectedHeightFirst, GridUnitType.Star)); + Assert.Equal(rowDefinitions[2].Height, new GridLength(expectedHeightSecond, GridUnitType.Star)); + } + + [Theory] + [InlineData(Key.Left, 90, 110)] + [InlineData(Key.Right, 110, 90)] + public void Horizontal_Keyboard_Input_Can_Move_Splitter(Key key, double expectedWidthFirst, double expectedWidthSecond) + { + var control1 = new Border { [Grid.ColumnProperty] = 0 }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, KeyboardIncrement = 10d }; + var control2 = new Border { [Grid.ColumnProperty] = 2 }; + + var columnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) + }; + + var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = grid + }; + + root.Measure(new Size(200, 200)); + root.Arrange(new Rect(0, 0, 200, 200)); + + splitter.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = key + }); + + Assert.Equal(columnDefinitions[0].Width, new GridLength(expectedWidthFirst, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(expectedWidthSecond, GridUnitType.Star)); + } + + [Fact] + public void Pressing_Escape_Key_Cancels_Resizing() + { + var control1 = new Border { [Grid.ColumnProperty] = 0 }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, KeyboardIncrement = 10d }; + var control2 = new Border { [Grid.ColumnProperty] = 2 }; + + var columnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) + }; + + var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } }; + + var root = new TestRoot + { + Child = grid + }; + + root.Measure(new Size(200, 200)); + root.Arrange(new Rect(0, 0, 200, 200)); + + splitter.RaiseEvent( + new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(-100, 0) + }); + + Assert.Equal(columnDefinitions[0].Width, new GridLength(0, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(200, GridUnitType.Star)); + + splitter.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Escape + }); + + Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); + Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); + } } } From 6dd6f336da5509d517c3e0c1935ea322ca2e7f0e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 23:59:17 +0100 Subject: [PATCH 031/126] Restore original delta constraints algorithm. --- src/Avalonia.Controls/GridSplitter.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 56a28e15c2..a2fefa0548 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -554,22 +554,9 @@ namespace Avalonia.Controls definition2Min = Math.Max(definition2Min, _resizeData.SplitterLength); } - if (_resizeData.SplitBehavior == SplitBehavior.Split) - { - // Determine the minimum and maximum the columns can be resized. - minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len); - maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min); - } - else if (_resizeData.SplitBehavior == SplitBehavior.Resize1) - { - minDelta = definition1Min - definition1Len; - maxDelta = definition1Max - definition1Len; - } - else - { - minDelta = definition2Len - definition2Max; - maxDelta = definition2Len - definition2Min; - } + // Determine the minimum and maximum the columns can be resized. + minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len); + maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min); } /// From 58b79a7724f1d492758a60bc7723abbc65b3be3b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 18:47:18 +0100 Subject: [PATCH 032/126] Validate min/max in MathUtilities.Clamp. --- src/Avalonia.Base/Utilities/MathUtilities.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 41b57b6e70..027028480c 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -159,6 +159,11 @@ namespace Avalonia.Utilities /// The clamped value. public static int Clamp(int val, int min, int max) { + if (min > max) + { + throw new ArgumentException($"{min} cannot be greater than {max}."); + } + if (val < min) { return min; From 0a8915b1cc9ffb2868a77e287527cb492dc272d6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 19:55:23 +0100 Subject: [PATCH 033/126] Added failing test for #3148 --- .../Primitives/SelectingItemsControlTests.cs | 61 +++++++++++++++++++ tests/Avalonia.UnitTests/TestRoot.cs | 11 ++++ 2 files changed, 72 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 17f0e609a5..e5bbde2eba 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.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.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -14,6 +15,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Data; +using Avalonia.Styling; using Avalonia.UnitTests; using Moq; using Xunit; @@ -980,6 +982,45 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(raised); } + [Fact] + public void AutoScrollToSelectedItem_On_Reset_Works() + { + // Issue #3148 + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new ResettingCollection(100); + + var target = new ListBox + { + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => + new TextBlock + { + Text = x, + Width = 100, + Height = 10 + }), + AutoScrollToSelectedItem = true, + VirtualizationMode = ItemVirtualizationMode.Simple, + }; + + var root = new TestRoot(true, target); + root.Measure(new Size(100, 100)); + root.Arrange(new Rect(0, 0, 100, 100)); + + Assert.True(target.Presenter.Panel.Children.Count > 0); + Assert.True(target.Presenter.Panel.Children.Count < 100); + + target.SelectedItem = "Item99"; + + // #3148 triggered here. + items.Reset(new[] { "Item99" }); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(1, target.Presenter.Panel.Children.Count); + } + } + [Fact] public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization() { @@ -1028,6 +1069,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Name = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], + [~ItemsPresenter.VirtualizationModeProperty] = control[~ListBox.VirtualizationModeProperty], }.RegisterInNameScope(scope)); } @@ -1072,5 +1114,24 @@ namespace Avalonia.Controls.UnitTests.Primitives return base.MoveSelection(direction, wrap); } } + + private class ResettingCollection : List, INotifyCollectionChanged + { + public ResettingCollection(int itemCount) + { + AddRange(Enumerable.Range(0, itemCount).Select(x => $"Item{x}")); + } + + public void Reset(IEnumerable items) + { + Clear(); + AddRange(items); + CollectionChanged?.Invoke( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + } } } diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 969d7bc821..56d7f028f2 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -24,8 +24,19 @@ namespace Avalonia.UnitTests } public TestRoot(IControl child) + : this(false, child) + { + Child = child; + } + + public TestRoot(bool useGlobalStyles, IControl child) : this() { + if (useGlobalStyles) + { + StylingParent = UnitTestApplication.Current; + } + Child = child; } From 4f41a70455516f9ee73c87212ea7913a038deb97 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 21:45:06 +0100 Subject: [PATCH 034/126] Make sure ItemsPresenter is updated before selection change. Having a separate subscription to `Items.CollectionChanged` in both `ItemsControl` and `ItemsPresenter` meant that the `ItemsPresenter` sometimes doesn't update itself in time for a selection change, resulting in #3148. Make `ItemsControl` trigger `ItemsPresenter.ItemsChanged` rather than both controls listening to the event separately. --- src/Avalonia.Controls/ItemsControl.cs | 6 ++++-- .../Presenters/IItemsPresenter.cs | 4 ++++ .../Presenters/ItemsPresenterBase.cs | 15 +++++++++++++-- .../Primitives/SelectingItemsControl.cs | 17 +++++++++++++---- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index b027da6d0c..b93346792d 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -356,6 +356,7 @@ namespace Avalonia.Controls var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; + Presenter?.ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); UpdateItemCount(); RemoveControlItemsFromLogicalChildren(oldValue); AddControlItemsToLogicalChildren(newValue); @@ -370,6 +371,9 @@ namespace Avalonia.Controls /// The event args. protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + UpdateItemCount(); + Presenter?.ItemsChanged(e); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -381,8 +385,6 @@ namespace Avalonia.Controls break; } - UpdateItemCount(); - var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); PseudoClasses.Set(":singleitem", collection != null && collection.Count == 1); diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 42311dc781..21a03402a0 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -1,12 +1,16 @@ // 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.Specialized; + namespace Avalonia.Controls.Presenters { public interface IItemsPresenter : IPresenter { IPanel Panel { get; } + void ItemsChanged(NotifyCollectionChangedEventArgs e); + void ScrollIntoView(object item); } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 0f0cdc37cf..ef1f277162 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -63,7 +63,7 @@ namespace Avalonia.Controls.Presenters _itemsSubscription?.Dispose(); _itemsSubscription = null; - if (_createdPanel && value is INotifyCollectionChanged incc) + if (!IsHosted && _createdPanel && value is INotifyCollectionChanged incc) { _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } @@ -130,6 +130,8 @@ namespace Avalonia.Controls.Presenters private set; } + protected bool IsHosted => TemplatedParent is IItemsPresenterHost; + /// public override sealed void ApplyTemplate() { @@ -144,6 +146,15 @@ namespace Avalonia.Controls.Presenters { } + /// + void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e) + { + if (Panel != null) + { + ItemsChanged(e); + } + } + /// /// Creates the for the control. /// @@ -215,7 +226,7 @@ namespace Avalonia.Controls.Presenters _createdPanel = true; - if (_itemsSubscription == null && Items is INotifyCollectionChanged incc) + if (!IsHosted && _itemsSubscription == null && Items is INotifyCollectionChanged incc) { _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7fddee1012..abad53f0d6 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -302,13 +302,24 @@ namespace Avalonia.Controls.Primitives /// protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - base.ItemsCollectionChanged(sender, e); - if (_updateCount > 0) { + base.ItemsCollectionChanged(sender, e); return; } + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); + break; + case NotifyCollectionChangedAction.Remove: + _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); + break; + } + + base.ItemsCollectionChanged(sender, e); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -318,14 +329,12 @@ namespace Avalonia.Controls.Primitives } else { - _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); UpdateSelectedItem(_selection.First(), false); } break; case NotifyCollectionChangedAction.Remove: - _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); UpdateSelectedItem(_selection.First(), false); ResetSelectedItems(); break; From 5fea821c94e210f72c4f8c0c1aa812a6ce8a4c14 Mon Sep 17 00:00:00 2001 From: mstr2 Date: Mon, 28 Oct 2019 22:48:12 +0100 Subject: [PATCH 035/126] Failing test for bug #3170 --- .../AvaloniaObjectTests_Validation.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index 2cf1eb1a97..f0e93dbb3a 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -78,6 +78,18 @@ namespace Avalonia.Base.UnitTests Assert.Equal(10, target.GetValue(Class1.AttachedProperty)); } + [Fact] + public void PropertyChanged_Event_Uses_Coerced_Value() + { + var inst = new Class1(); + inst.PropertyChanged += (sender, e) => + { + Assert.Equal(10, e.NewValue); + }; + + inst.SetValue(Class1.QuxProperty, 15); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty QuxProperty = From e46cb674234d60cae9da83d11733c98617722c3e Mon Sep 17 00:00:00 2001 From: mstr2 Date: Mon, 28 Oct 2019 22:50:01 +0100 Subject: [PATCH 036/126] Fixed a bug where ValueStore.Changed could be called with pre-validation value --- src/Avalonia.Base/ValueStore.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 1bdbd4ca7c..e06c5996c9 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -57,7 +57,8 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - _propertyValues.SetValue(property, Validate(property, value)); + Validate(property, ref value); + _propertyValues.SetValue(property, value); Changed(property, priority, v, value); return; } @@ -78,7 +79,8 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - _propertyValues.AddValue(property, Validate(property, value)); + Validate(property, ref value); + _propertyValues.AddValue(property, value); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } @@ -166,16 +168,14 @@ namespace Avalonia validate2); } - private object Validate(AvaloniaProperty property, object value) + private void Validate(AvaloniaProperty property, ref object value) { var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); if (validate != null && value != AvaloniaProperty.UnsetValue) { - return validate(_owner, value); + value = validate(_owner, value); } - - return value; } private DeferredSetter GetDeferredSetter(AvaloniaProperty property) From e27c7c0fff35b04663cff08bdb75e848e8501990 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 29 Oct 2019 10:21:14 +0300 Subject: [PATCH 037/126] Fixed parameter order in OnSelectionChanged in AutoCompleteBox Apparently Silverlight has different constructor argument order --- src/Avalonia.Controls/AutoCompleteBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index ce4358648b..64db832a81 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -704,7 +704,7 @@ namespace Avalonia.Controls added.Add(e.NewValue); } - OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added)); + OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, added, removed)); } /// From 96619e44408aea5319a80b40327d67cee94c4615 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Oct 2019 14:49:19 +0100 Subject: [PATCH 038/126] Added failing test. `ItemsPresenter.Items` isn't correctly in sync with `ItemsControl.Items` when assigning new `Items`. --- .../ItemsControlTests.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index b2839360ee..c338d29e96 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -522,6 +522,36 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Presenter_Items_Should_Be_In_Sync() + { + var target = new ItemsControl + { + Template = GetTemplate(), + Items = new object[] + { + new Button(), + new Button(), + }, + }; + + var root = new TestRoot { Child = target }; + var otherPanel = new StackPanel(); + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.ItemContainerGenerator.Materialized += (s, e) => + { + Assert.IsType(e.Containers[0].Item); + }; + + target.Items = new[] + { + new Canvas() + }; + } + private class Item { public Item(string value) From 496fae20e55995017faf8f6f5672c507dc45fd15 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Oct 2019 14:59:47 +0100 Subject: [PATCH 039/126] Ensure Items are synchronised with Presenter. Ensure that the `ItemsControl`'s presenter is notified of an `Items` change at the appropriate time. --- src/Avalonia.Controls/ItemsControl.cs | 7 ++++++- src/Avalonia.Controls/Presenters/IItemsPresenter.cs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index b93346792d..1203792559 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -356,10 +356,15 @@ namespace Avalonia.Controls var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; - Presenter?.ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); UpdateItemCount(); RemoveControlItemsFromLogicalChildren(oldValue); AddControlItemsToLogicalChildren(newValue); + + if (Presenter != null) + { + Presenter.Items = newValue; + } + SubscribeToItems(newValue); } diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 21a03402a0..c4acf1ebef 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -1,12 +1,15 @@ // 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; using System.Collections.Specialized; namespace Avalonia.Controls.Presenters { public interface IItemsPresenter : IPresenter { + IEnumerable Items { get; set; } + IPanel Panel { get; } void ItemsChanged(NotifyCollectionChangedEventArgs e); From 9107c0e96eea42756f7d6ea26013979986e7adfe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Oct 2019 23:01:18 +0100 Subject: [PATCH 040/126] Added another failing test. Adapted from failing test described by @MarchingCube in PR feedback: https://github.com/AvaloniaUI/Avalonia/pull/3177#issuecomment-547515972 --- .../ItemsControlTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index c338d29e96..227d783874 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -12,6 +12,7 @@ using Xunit; using System.Collections.ObjectModel; using Avalonia.UnitTests; using Avalonia.Input; +using System.Collections.Generic; namespace Avalonia.Controls.UnitTests { @@ -104,6 +105,28 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new[] { child }, target.GetLogicalChildren()); } + [Fact] + public void Added_Container_Should_Have_LogicalParent_Set_To_ItemsControl() + { + var item = new Border(); + var items = new ObservableCollection(); + + var target = new ItemsControl + { + Template = GetTemplate(), + Items = items, + }; + + var root = new TestRoot(true, target); + + root.Measure(new Size(100, 100)); + root.Arrange(new Rect(0, 0, 100, 100)); + + items.Add(item); + + Assert.Equal(target, item.Parent); + } + [Fact] public void Control_Item_Should_Be_Removed_From_Logical_Children_Before_ApplyTemplate() { From 68b655b5177175fc1b168b2130064992575c1fb6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Oct 2019 07:32:38 +0100 Subject: [PATCH 041/126] Add to logical children before notifying presenter. This makes sure that newly added control items have the correct logical parent. --- src/Avalonia.Controls/ItemsControl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1203792559..bf22f0a08a 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -377,7 +377,6 @@ namespace Avalonia.Controls protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { UpdateItemCount(); - Presenter?.ItemsChanged(e); switch (e.Action) { @@ -390,6 +389,8 @@ namespace Avalonia.Controls break; } + Presenter?.ItemsChanged(e); + var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); PseudoClasses.Set(":singleitem", collection != null && collection.Count == 1); From 4d14218a653d2cdd0655d38d47b4af84092fc4f1 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 30 Oct 2019 06:18:22 +0100 Subject: [PATCH 042/126] Failing unit tests #3180 --- .../TabControlTests.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index ddf7e7a0fa..1e2c2084bd 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -44,6 +45,31 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(selected, target.SelectedItem); } + [Fact] + public void Pre_Selecting_TabItem_Should_Set_SelectedContent_After_It_Was_Added() + { + var target = new TabControl + { + Template = TabControlTemplate(), + }; + + const string secondContent = "Second"; + + var items = new AvaloniaList + { + new TabItem { Header = "First"}, + new TabItem { Header = "Second", Content = secondContent, IsSelected = true } + }; + + target.Items = items; + + target.ApplyTemplate(); + + target.Measure(Size.Infinity); + + Assert.Equal(secondContent, target.SelectedContent); + } + [Fact] public void Logical_Children_Should_Be_TabItems() { From b20f3f4cdfa0f2de0a63e8caf9891a0e9af1e215 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 30 Oct 2019 09:20:50 +0100 Subject: [PATCH 043/126] Update selected context when selected index changes --- .../Primitives/SelectingItemsControl.cs | 2 +- src/Avalonia.Controls/TabControl.cs | 59 +++++++++++++++++++ src/Avalonia.Controls/TabItem.cs | 19 ------ 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7fddee1012..a030e2cd9a 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -358,7 +358,7 @@ namespace Avalonia.Controls.Primitives { if ((container.ContainerControl as ISelectable)?.IsSelected == true) { - if (SelectedIndex == -1) + if (SelectionMode.HasFlag(SelectionMode.Single)) { SelectedIndex = container.Index; } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 61ac0822b0..a2227d66e8 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -70,6 +70,7 @@ namespace Avalonia.Controls SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); + SelectedIndexProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); } /// @@ -145,6 +146,64 @@ namespace Avalonia.Controls return RegisterContentPresenter(presenter); } + private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) + { + var index = (int)e.NewValue; + + if (index == -1) + { + SelectedContentTemplate = null; + + SelectedContent = null; + + return; + } + + var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index); + + if (container == null) + { + if (Items is AvaloniaList items) + { + container = items[index] as TabItem; + } + } + + if (container == null) + { + return; + } + + UpdateSelectedContent(container); + } + + private void UpdateSelectedContent(TabItem item) + { + if (SelectedContentTemplate != item.ContentTemplate) + { + SelectedContentTemplate = item.ContentTemplate; + } + + if (SelectedContent != item.Content) + { + SelectedContent = item.Content; + } + } + + protected override Size MeasureOverride(Size availableSize) + { + var size = base.MeasureOverride(availableSize); + + if (SelectedIndex != -1 && SelectedContent == null) + { + var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); + + UpdateSelectedContent(container); + } + + return size; + } + /// /// Called when an is registered with the control. /// diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index fca1e022aa..0160bfcec1 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -30,7 +30,6 @@ namespace Avalonia.Controls { SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); - IsSelectedProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent(e)); DataContextProperty.Changed.AddClassHandler((x, e) => x.UpdateHeader(e)); } @@ -83,23 +82,5 @@ namespace Avalonia.Controls } } } - - private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) - { - if (!IsSelected) - { - return; - } - - if (ParentTabControl.SelectedContentTemplate != ContentTemplate) - { - ParentTabControl.SelectedContentTemplate = ContentTemplate; - } - - if (ParentTabControl.SelectedContent != Content) - { - ParentTabControl.SelectedContent = Content; - } - } } } From 16f2bcdb9bc95b8e16b502c398d540dbf038603f Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 30 Oct 2019 09:27:10 +0100 Subject: [PATCH 044/126] Remove redundant ParentTabControl property --- src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs | 2 -- src/Avalonia.Controls/TabItem.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs index a6a64e570b..1c5419735b 100644 --- a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs @@ -19,8 +19,6 @@ namespace Avalonia.Controls.Generators { var tabItem = (TabItem)base.CreateContainer(item); - tabItem.ParentTabControl = Owner; - tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty]; if (tabItem.HeaderTemplate == null) diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 0160bfcec1..e27977bf3d 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -53,8 +53,6 @@ namespace Avalonia.Controls set { SetValue(IsSelectedProperty, value); } } - internal TabControl ParentTabControl { get; set; } - private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj) { if (Header == null) From b7a3747da66b5d0feae56dc10636c0f10b695ed2 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Wed, 30 Oct 2019 23:26:34 +0100 Subject: [PATCH 045/126] Add failing test for https://github.com/AvaloniaUI/Avalonia/issues/3179. --- .../Rendering/DeferredRendererTests.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index b4743e900d..6063a382a0 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -369,6 +369,81 @@ namespace Avalonia.Visuals.UnitTests.Rendering } + [Fact] + public void Should_Update_VisualNodes_When_Child_Moved_To_New_Parent_And_New_Root() + { + var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); + + Decorator moveFrom; + Decorator moveTo; + Canvas moveMe; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + (moveFrom = new Decorator + { + Child = moveMe = new Canvas(), + }) + } + } + }; + + var otherRoot = new TestRoot + { + Child = new StackPanel + { + Children = + { + (moveTo = new Decorator()) + } + } + }; + + var sceneBuilder = new SceneBuilder(); + var target = new DeferredRenderer( + root, + loop.Object, + sceneBuilder: sceneBuilder, + dispatcher: dispatcher); + + var otherSceneBuilder = new SceneBuilder(); + var otherTarget = new DeferredRenderer( + otherRoot, + loop.Object, + sceneBuilder: otherSceneBuilder, + dispatcher: dispatcher); + + root.Renderer = target; + otherRoot.Renderer = otherTarget; + + target.Start(); + otherTarget.Start(); + + RunFrame(target); + RunFrame(otherTarget); + + moveFrom.Child = null; + moveTo.Child = moveMe; + + RunFrame(target); + RunFrame(otherTarget); + + var scene = target.UnitTestScene(); + var otherScene = otherTarget.UnitTestScene(); + + var moveFromNode = (VisualNode)scene.FindNode(moveFrom); + var moveToNode = (VisualNode)otherScene.FindNode(moveTo); + + Assert.Empty(moveFromNode.Children); + Assert.Equal(1, moveToNode.Children.Count); + Assert.Same(moveMe, moveToNode.Children[0].Visual); + } + [Fact] public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity() { From 5df55e5c9f42721a4917fb6228bead4349e9d450 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 31 Oct 2019 13:21:46 +0100 Subject: [PATCH 046/126] Fix multiselection --- .../Primitives/SelectingItemsControl.cs | 10 +++--- src/Avalonia.Controls/TabControl.cs | 33 +++++++++++-------- .../SelectingItemsControlTests_Multiple.cs | 1 + .../TabControlTests.cs | 4 +-- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a030e2cd9a..5a176d733d 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -358,17 +358,17 @@ namespace Avalonia.Controls.Primitives { if ((container.ContainerControl as ISelectable)?.IsSelected == true) { - if (SelectionMode.HasFlag(SelectionMode.Single)) - { - SelectedIndex = container.Index; - } - else + if (SelectionMode.HasFlag(SelectionMode.Multiple)) { if (_selection.Add(container.Index)) { resetSelectedItems = true; } } + else + { + SelectedIndex = container.Index; + } MarkContainerSelected(container.ContainerControl, true); } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index a2227d66e8..87ba282db0 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -146,6 +146,25 @@ namespace Avalonia.Controls return RegisterContentPresenter(presenter); } + protected override void OnContainersMaterialized(ItemContainerEventArgs e) + { + base.OnContainersMaterialized(e); + + if (SelectedContent != null || SelectedIndex == -1) + { + return; + } + + var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); + + if (container == null) + { + return; + } + + UpdateSelectedContent(container); + } + private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) { var index = (int)e.NewValue; @@ -190,20 +209,6 @@ namespace Avalonia.Controls } } - protected override Size MeasureOverride(Size availableSize) - { - var size = base.MeasureOverride(availableSize); - - if (SelectedIndex != -1 && SelectedContent == null) - { - var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex); - - UpdateSelectedContent(container); - } - - return size; - } - /// /// Called when an is registered with the control. /// diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index be0f4272a5..952a00a14e 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1080,6 +1080,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var target = new TestSelector { Items = items, + SelectionMode = SelectionMode.Multiple, Template = Template(), }; diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 1e2c2084bd..2f8c974802 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -63,9 +63,7 @@ namespace Avalonia.Controls.UnitTests target.Items = items; - target.ApplyTemplate(); - - target.Measure(Size.Infinity); + ApplyTemplate(target); Assert.Equal(secondContent, target.SelectedContent); } From d59d0904f43997a8cc4dcbb6064d141543045091 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 11:20:33 +0200 Subject: [PATCH 047/126] add test for tooltip --- .../ToolTipTests.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/ToolTipTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs new file mode 100644 index 0000000000..2a1bb78c2e --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -0,0 +1,45 @@ +// 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 Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class TolTipTests + { + private MouseTestHelper _mouseHelper = new MouseTestHelper(); + + [Fact] + public void Should_Open_On_Pointer_Enter() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + + var target = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 0 + }; + + window.Content = target; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.True((target as IVisual).IsAttachedToVisualTree); + + _mouseHelper.Enter(target); + + Assert.True(ToolTip.GetIsOpen(target)); + } + } + } +} From 60a596bd82ebb387da5047f647e1388d3afb0b85 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 14:52:08 +0200 Subject: [PATCH 048/126] add another test for tooltip --- .../ToolTipTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index 2a1bb78c2e..eba0d7cb3e 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -41,5 +41,51 @@ namespace Avalonia.Controls.UnitTests Assert.True(ToolTip.GetIsOpen(target)); } } + + [Fact] + public void Should_Open_On_Pointer_Enter_With_Delay() + { + Action timercallback = null; + var delay = TimeSpan.Zero; + + var pti = Mock.Of(x => x.CurrentThreadIsLoopThread == true); + + Mock.Get(pti) + .Setup(v => v.StartTimer(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((priority, interval, tick) => + { + delay = interval; + timercallback = tick; + }) + .Returns(Disposable.Empty); + + using (UnitTestApplication.Start(TestServices.StyledWindow.With(threadingInterface: pti))) + { + var window = new Window(); + + var target = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 1 + }; + + window.Content = target; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.True((target as IVisual).IsAttachedToVisualTree); + + _mouseHelper.Enter(target); + + Assert.Equal(TimeSpan.FromMilliseconds(1), delay); + Assert.NotNull(timercallback); + Assert.False(ToolTip.GetIsOpen(target)); + + timercallback(); + + Assert.True(ToolTip.GetIsOpen(target)); + } + } } } From 2288b7ab1d0463f84032c8907eb4b1fae671840b Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 14:52:59 +0200 Subject: [PATCH 049/126] add failing test for tooltip for issue #3188 --- .../ToolTipTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index eba0d7cb3e..362dd6d111 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -16,6 +16,24 @@ namespace Avalonia.Controls.UnitTests { private MouseTestHelper _mouseHelper = new MouseTestHelper(); + [Fact] + public void Should_Not_Open_On_Detached_Control() + { + //issue #3188 + var control = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 0 + }; + + Assert.False((control as IVisual).IsAttachedToVisualTree); + + //here in issue #3188 exception is raised + _mouseHelper.Enter(control); + + Assert.False(ToolTip.GetIsOpen(control)); + } + [Fact] public void Should_Open_On_Pointer_Enter() { From a3dbf72947d4e971325cd2870b19e99cd4ae65c5 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 15:03:33 +0200 Subject: [PATCH 050/126] don't try open tooltip on detached control --- src/Avalonia.Controls/ToolTipService.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index 384a9db0cf..d90729e8a5 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Input; using Avalonia.Threading; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -79,7 +80,10 @@ namespace Avalonia.Controls { StopTimer(); - ToolTip.SetIsOpen(control, true); + if ((control as IVisual).IsAttachedToVisualTree) + { + ToolTip.SetIsOpen(control, true); + } } private void Close(Control control) From f933fe53cfd8d5e029e930b81c8a7f36c5640e9a Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 31 Oct 2019 14:13:58 +0100 Subject: [PATCH 051/126] Add failing test #2699 --- .../TabControlTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index ddf7e7a0fa..b65a4d67d4 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -287,6 +288,25 @@ namespace Avalonia.Controls.UnitTests Assert.Single(target.GetLogicalChildren(), content); } + [Fact] + public void Should_Not_Propagate_DataContext_To_TabItem_Content() + { + var dataContext = "DataContext"; + + var tabItem = new TabItem(); + + var target = new TabControl + { + Template = TabControlTemplate(), + DataContext = dataContext, + Items = new AvaloniaList { tabItem } + }; + + ApplyTemplate(target); + + Assert.NotEqual(dataContext, tabItem.Content); + } + private IControlTemplate TabControlTemplate() { return new FuncControlTemplate((parent, scope) => From e9335d6a2c4755bd120e7da06e2dec0029f93f54 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 31 Oct 2019 14:15:11 +0100 Subject: [PATCH 052/126] Fix #2699 --- .../Generators/TabItemContainerGenerator.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs index a6a64e570b..d99648a158 100644 --- a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs @@ -48,11 +48,6 @@ namespace Avalonia.Controls.Generators tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty]; } - if (tabItem.Content == null) - { - tabItem[~ContentControl.ContentProperty] = tabItem[~StyledElement.DataContextProperty]; - } - return tabItem; } } From 83bfb8ec70837d260527b3724c68f4da0eef9243 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Nov 2019 11:07:08 +0100 Subject: [PATCH 053/126] Initial implementation of a font manager --- Avalonia.sln | 3 +- build/HarfBuzzSharp.props | 6 + build/SkiaSharp.props | 4 +- .../Presenters/TextPresenter.cs | 3 +- src/Avalonia.Controls/TextBlock.cs | 5 +- src/Avalonia.Visuals/Media/FontFamily.cs | 23 ++- src/Avalonia.Visuals/Media/FontManager.cs | 100 ++++++++++ .../Media/Fonts/FamilyNameCollection.cs | 6 +- src/Avalonia.Visuals/Media/FormattedText.cs | 47 ++++- src/Avalonia.Visuals/Media/GlyphTypeface.cs | 108 ++++++++++ src/Avalonia.Visuals/Media/Typeface.cs | 99 ++++++--- .../Platform/IFontManagerImpl.cs | 57 ++++++ .../Platform/IGlyphTypefaceImpl.cs | 89 +++++++++ .../Platform/IPlatformRenderInterface.cs | 7 +- .../Rendering/RendererBase.cs | 6 +- src/Skia/Avalonia.Skia/Avalonia.Skia.csproj | 1 + src/Skia/Avalonia.Skia/FontKey.cs | 40 ++++ src/Skia/Avalonia.Skia/FontManagerImpl.cs | 91 +++++++++ src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 48 ++--- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 179 +++++++++++++++++ .../Avalonia.Skia/PlatformRenderInterface.cs | 5 +- .../Avalonia.Skia/SKTypefaceCollection.cs | 91 ++------- .../SKTypefaceCollectionCache.cs | 8 +- src/Skia/Avalonia.Skia/SkiaPlatform.cs | 5 + src/Skia/Avalonia.Skia/TypefaceCache.cs | 86 ++------ .../Avalonia.Skia/TypefaceCollectionEntry.cs | 19 ++ .../Avalonia.Direct2D1.csproj | 1 + .../Avalonia.Direct2D1/Direct2D1Platform.cs | 17 +- .../Media/Direct2D1FontCollectionCache.cs | 49 +++-- .../Media/FontManagerImpl.cs | 80 ++++++++ .../Media/FormattedTextImpl.cs | 19 +- .../Media/GlyphTypefaceImpl.cs | 188 ++++++++++++++++++ .../FullLayoutTests.cs | 1 + .../Media/FormattedTextImplTests.cs | 3 +- .../MockPlatformRenderInterface.cs | 3 +- tests/Avalonia.UnitTests/TestServices.cs | 1 + .../Media/FontFamilyTests.cs | 44 +++- .../Media/TypefaceTests.cs | 14 +- .../VisualTree/MockRenderInterface.cs | 1 + 39 files changed, 1262 insertions(+), 295 deletions(-) create mode 100644 build/HarfBuzzSharp.props create mode 100644 src/Avalonia.Visuals/Media/FontManager.cs create mode 100644 src/Avalonia.Visuals/Media/GlyphTypeface.cs create mode 100644 src/Avalonia.Visuals/Platform/IFontManagerImpl.cs create mode 100644 src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs create mode 100644 src/Skia/Avalonia.Skia/FontKey.cs create mode 100644 src/Skia/Avalonia.Skia/FontManagerImpl.cs create mode 100644 src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs create mode 100644 src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs create mode 100644 src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs create mode 100644 src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs diff --git a/Avalonia.sln b/Avalonia.sln index 568a16ce0e..e40ebae4d6 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -128,6 +128,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Base.props = build\Base.props build\Binding.props = build\Binding.props build\BuildTargets.targets = build\BuildTargets.targets + build\HarfBuzzSharp.props = build\HarfBuzzSharp.props build\JetBrains.Annotations.props = build\JetBrains.Annotations.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props @@ -201,7 +202,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props new file mode 100644 index 0000000000..f8767c7599 --- /dev/null +++ b/build/HarfBuzzSharp.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index c03ad0fefd..796bd8e596 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 5931fec350..e0cc9aa128 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -297,7 +297,8 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Text = "X", - Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), + Typeface = new Typeface(FontFamily, FontWeight, FontStyle), + FontSize = FontSize, TextAlignment = TextAlignment, Constraint = availableSize, }.Bounds.Size; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index b9603b91ed..c7855ddfd1 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -352,10 +352,11 @@ namespace Avalonia.Controls return new FormattedText { Constraint = constraint, - Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), + Typeface = new Typeface(FontFamily, FontWeight, FontStyle), + FontSize = FontSize, Text = text ?? string.Empty, TextAlignment = TextAlignment, - Wrapping = TextWrapping, + TextWrapping = TextWrapping, }; } diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index a486723d86..665dfc1129 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -5,12 +5,16 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.Media.Fonts; -using Avalonia.Platform; namespace Avalonia.Media { public class FontFamily { + static FontFamily() + { + Default = new FontFamily(FontManager.Default.DefaultFontFamilyName); + } + /// /// /// Initializes a new instance of the class. @@ -30,9 +34,7 @@ namespace Avalonia.Media { if (string.IsNullOrEmpty(name)) { - FamilyNames = new FamilyNameCollection(string.Empty); - - return; + throw new ArgumentNullException(nameof(name)); } var fontFamilySegment = GetFontFamilyIdentifier(name); @@ -53,13 +55,13 @@ namespace Avalonia.Media /// /// Represents the default font family /// - public static FontFamily Default => new FontFamily(string.Empty); + public static FontFamily Default { get; } /// /// Represents all font families in the system. This can be an expensive call depending on platform implementation. /// public static IEnumerable SystemFontFamilies => - AvaloniaLocator.Current.GetService().InstalledFontNames.Select(name => new FontFamily(name)); + FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); /// /// Gets the primary family name of the font family. @@ -181,7 +183,14 @@ namespace Avalonia.Media { var hash = (int)2186146271; - hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); + if (Key != null) + { + hash = (hash * 15768619) ^ Key.GetHashCode(); + } + else + { + hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); + } if (Key != null) { diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs new file mode 100644 index 0000000000..e89471ede8 --- /dev/null +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -0,0 +1,100 @@ +// 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.Globalization; +using Avalonia.Platform; + +namespace Avalonia.Media +{ + public abstract class FontManager : IFontManagerImpl + { + public static readonly FontManager Default = CreateDefaultFontManger(); + + /// + public string DefaultFontFamilyName { get; protected set; } + + private static FontManager CreateDefaultFontManger() + { + var platformImpl = AvaloniaLocator.Current.GetService(); + + if(platformImpl == null) + { + return new EmptyFontManager(); + } + + return new PlatformFontManger(platformImpl); + } + + /// + public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + + /// + public abstract IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); + + /// + public abstract Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); + + /// + public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, + FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null); + + private class PlatformFontManger : FontManager + { + private readonly IFontManagerImpl _platformImpl; + + public PlatformFontManger(IFontManagerImpl platformImpl) + { + _platformImpl = platformImpl; + + DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName; + } + + public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + _platformImpl.GetInstalledFontFamilyNames(checkForUpdates); + + public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) => _platformImpl.CreateGlyphTypeface(typeface); + + public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => + _platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle); + + public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, + FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null) => + _platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture); + } + + private class EmptyFontManager : FontManager + { + private readonly string[] _defaultFontFamilies = { "Arial" }; + + public EmptyFontManager() + { + DefaultFontFamilyName = "Arial"; + } + + public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + { + return _defaultFontFamilies; + } + + public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + throw new NotSupportedException(); + } + + public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + { + throw new NotSupportedException(); + } + + public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null) + { + throw new NotSupportedException(); + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index acf0bbdb11..0c161131dc 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -9,7 +9,7 @@ using System.Text; namespace Avalonia.Media.Fonts { - public class FamilyNameCollection : IEnumerable + public class FamilyNameCollection : IReadOnlyList { /// /// Initializes a new instance of the class. @@ -130,5 +130,9 @@ namespace Avalonia.Media.Fonts return other.ToString().Equals(ToString()); } + + public int Count => Names.Count; + + public string this[int index] => Names[index]; } } diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index e20e03e296..5013f925b3 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/src/Avalonia.Visuals/Media/FormattedText.cs @@ -16,9 +16,10 @@ namespace Avalonia.Media private IFormattedTextImpl _platformImpl; private IReadOnlyList _spans; private Typeface _typeface; + private double _fontSize; private string _text; private TextAlignment _textAlignment; - private TextWrapping _wrapping; + private TextWrapping _textWrapping; /// /// Initializes a new instance of the class. @@ -37,6 +38,31 @@ namespace Avalonia.Media _platform = platform; } + /// + /// + /// + /// + /// + /// + /// + /// + /// + public FormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment, + TextWrapping textWrapping, Size constraint) + { + _text = text; + + _typeface = typeface; + + _fontSize = fontSize; + + _textAlignment = textAlignment; + + _textWrapping = textWrapping; + + _constraint = constraint; + } + /// /// Gets the bounds of the text within the . /// @@ -61,6 +87,16 @@ namespace Avalonia.Media set => Set(ref _typeface, value); } + + /// + /// Gets or sets the font size. + /// + public double FontSize + { + get => _fontSize; + set => Set(ref _fontSize, value); + } + /// /// Gets or sets a collection of spans that describe the formatting of subsections of the /// text. @@ -92,10 +128,10 @@ namespace Avalonia.Media /// /// Gets or sets the text wrapping. /// - public TextWrapping Wrapping + public TextWrapping TextWrapping { - get => _wrapping; - set => Set(ref _wrapping, value); + get => _textWrapping; + set => Set(ref _textWrapping, value); } /// @@ -110,8 +146,9 @@ namespace Avalonia.Media _platformImpl = _platform.CreateFormattedText( _text, _typeface, + _fontSize, _textAlignment, - _wrapping, + _textWrapping, _constraint, _spans); } diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs new file mode 100644 index 0000000000..1c959a86c5 --- /dev/null +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -0,0 +1,108 @@ +// 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.Platform; + +namespace Avalonia.Media +{ + public class GlyphTypeface : IDisposable + { + public GlyphTypeface(Typeface typeface) : this(FontManager.Default.CreateGlyphTypeface(typeface)) + { + } + + public GlyphTypeface(IGlyphTypefaceImpl platformImpl) + { + PlatformImpl = platformImpl; + } + + public IGlyphTypefaceImpl PlatformImpl { get; } + + /// + /// Gets the font design units per em. + /// + public short DesignEmHeight => PlatformImpl.DesignEmHeight; + + /// + /// Gets the recommended distance above the baseline in design em size. + /// + public int Ascent => PlatformImpl.Ascent; + + /// + /// Gets the recommended distance under the baseline in design em size. + /// + public int Descent => PlatformImpl.Descent; + + /// + /// Gets the recommended additional space between two lines of text in design em size. + /// + public int LineGap => PlatformImpl.LineGap; + + /// + /// Gets the recommended line height. + /// + public int LineHeight => Descent - Ascent + LineGap; + + /// + /// Gets a value that indicates the distance of the underline from the baseline in design em size. + /// + public int UnderlinePosition => PlatformImpl.UnderlinePosition; + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + public int UnderlineThickness => PlatformImpl.UnderlineThickness; + + /// + /// Gets a value that indicates the distance of the strikethrough from the baseline in design em size. + /// + public int StrikethroughPosition => PlatformImpl.StrikethroughPosition; + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + public int StrikethroughThickness => PlatformImpl.StrikethroughThickness; + + /// + /// Returns an glyph index for the specified codepoint. + /// + /// + /// Returns 0 if a glyph isn't found. + /// + /// The codepoint. + /// + /// A glyph index. + /// + public ushort GetGlyph(uint codepoint) => PlatformImpl.GetGlyph(codepoint); + + /// + /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0. + /// + /// The codepoints to map. + /// + public ushort[] GetGlyphs(ReadOnlySpan codepoints) => PlatformImpl.GetGlyphs(codepoints); + + /// + /// Returns the glyph advance for the specified glyph. + /// + /// The glyph. + /// + /// The advance. + /// + public int GetGlyphAdvance(ushort glyph) => PlatformImpl.GetGlyphAdvance(glyph); + + /// + /// Returns an array of glyph advances in design em size. + /// + /// The glyph indices. + /// + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) => PlatformImpl.GetGlyphAdvances(glyphs); + + void IDisposable.Dispose() + { + PlatformImpl?.Dispose(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 37ac0953bf..2d3c7e6ffa 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -1,39 +1,38 @@ -using System; +// 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.Diagnostics; +using JetBrains.Annotations; namespace Avalonia.Media { /// /// Represents a typeface. /// - public class Typeface + [DebuggerDisplay("Name = {FontFamily.Name}, Style = {Style}, Weight = {Weight}")] + public class Typeface : IEquatable { public static readonly Typeface Default = new Typeface(FontFamily.Default); + private GlyphTypeface _glyphTypeface; + /// /// Initializes a new instance of the class. /// /// The font family. - /// The font size, in DIPs. - /// The font style. /// The font weight. - public Typeface( - FontFamily fontFamily, - double fontSize = 12, - FontStyle style = FontStyle.Normal, - FontWeight weight = FontWeight.Normal) + /// The font style. + public Typeface([NotNull]FontFamily fontFamily, + FontWeight weight = FontWeight.Normal, + FontStyle style = FontStyle.Normal) { - if (fontSize <= 0) - { - throw new ArgumentException("Font size must be > 0."); - } - if (weight <= 0) { throw new ArgumentException("Font weight must be > 0."); } FontFamily = fontFamily; - FontSize = fontSize; Style = style; Weight = weight; } @@ -42,15 +41,12 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The name of the font family. - /// The font size, in DIPs. /// The font style. /// The font weight. - public Typeface( - string fontFamilyName, - double fontSize = 12, - FontStyle style = FontStyle.Normal, - FontWeight weight = FontWeight.Normal) - : this(new FontFamily(fontFamilyName), fontSize, style, weight) + public Typeface(string fontFamilyName, + FontWeight weight = FontWeight.Normal, + FontStyle style = FontStyle.Normal) + : this(new FontFamily(fontFamilyName), weight, style) { } @@ -59,11 +55,6 @@ namespace Avalonia.Media /// public FontFamily FontFamily { get; } - /// - /// Gets the size of the font in DIPs. - /// - public double FontSize { get; } - /// /// Gets the font style. /// @@ -73,5 +64,59 @@ namespace Avalonia.Media /// Gets the font weight. /// public FontWeight Weight { get; } + + /// + /// Gets the glyph typeface. + /// + /// + /// The glyph typeface. + /// + public GlyphTypeface GlyphTypeface => _glyphTypeface ?? (_glyphTypeface = new GlyphTypeface(this)); + + public static bool operator !=(Typeface a, Typeface b) + { + return !(a == b); + } + + public static bool operator ==(Typeface a, Typeface b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + return !(a is null) && a.Equals(b); + } + + public override bool Equals(object obj) + { + if (obj is Typeface typeface) + { + return Equals(typeface); + } + + return false; + } + + public bool Equals(Typeface other) + { + if (other is null) + { + return false; + } + + return FontFamily.Equals(other.FontFamily) && Style == other.Style && Weight == other.Weight; + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (FontFamily != null ? FontFamily.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (int)Style; + hashCode = (hashCode * 397) ^ (int)Weight; + return hashCode; + } + } } } diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs new file mode 100644 index 0000000000..236631edde --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -0,0 +1,57 @@ +// 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.Globalization; +using Avalonia.Media; + +namespace Avalonia.Platform +{ + public interface IFontManagerImpl + { + /// + /// Gets the system's default font family's name. + /// + string DefaultFontFamilyName { get; } + + /// + /// Get all installed fonts in the system. + /// If true the font collection is updated. + /// + IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + + /// + /// Creates a glyph typeface for specified typeface. + /// + /// The typeface. + /// + /// The glyph typeface implementation. + /// + IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); + + /// + /// Get a typeface from specified parameters. + /// + /// The font family. + /// The font weight. + /// The font style. + /// + /// The typeface. + /// + Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); + + /// + /// Tries to match a specified character to a typeface that supports specified font properties. + /// + /// The codepoint to match against. + /// The font weight. + /// The font style. + /// The font family. This is optional and used for fallback lookup. + /// The culture. + /// + /// The typeface. + /// + Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null); + } +} diff --git a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs new file mode 100644 index 0000000000..8c043a5129 --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs @@ -0,0 +1,89 @@ +// 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.Platform +{ + public interface IGlyphTypefaceImpl : IDisposable + { + /// + /// Gets the font design units per em. + /// + short DesignEmHeight { get; } + + /// + /// Gets the recommended distance above the baseline in design em size. + /// + int Ascent { get; } + + /// + /// Gets the recommended distance under the baseline in design em size. + /// + int Descent { get; } + + /// + /// Gets the recommended additional space between two lines of text in design em size. + /// + int LineGap { get; } + + /// + /// Gets a value that indicates the distance of the underline from the baseline in design em size. + /// + int UnderlinePosition { get; } + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + int UnderlineThickness { get; } + + /// + /// Gets a value that indicates the distance of the strikethrough from the baseline in design em size. + /// + int StrikethroughPosition { get; } + + /// + /// Gets a value that indicates the thickness of the underline in design em size. + /// + int StrikethroughThickness { get; } + + /// + /// Returns an glyph index for the specified codepoint. + /// + /// + /// Returns 0 if a glyph isn't found. + /// + /// The codepoint. + /// + /// A glyph index. + /// + ushort GetGlyph(uint codepoint); + + /// + /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0. + /// + /// The codepoints to map. + /// + /// An array of glyph indices. + /// + ushort[] GetGlyphs(ReadOnlySpan codepoints); + + /// + /// Returns the glyph advance for the specified glyph. + /// + /// The glyph. + /// + /// The advance. + /// + int GetGlyphAdvance(ushort glyph); + + /// + /// Returns an array of glyph advances in design em size. + /// + /// The glyph indices. + /// + /// An array of glyph advances. + /// + int[] GetGlyphAdvances(ReadOnlySpan glyphs); + } +} diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 87db9251e1..619d3088b4 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -13,16 +13,12 @@ namespace Avalonia.Platform /// public interface IPlatformRenderInterface { - /// - /// Get all installed fonts in the system - /// - IEnumerable InstalledFontNames { get; } - /// /// Creates a formatted text implementation. /// /// The text. /// The base typeface. + /// The font size. /// The text alignment. /// The text wrapping mode. /// The text layout constraints. @@ -31,6 +27,7 @@ namespace Avalonia.Platform IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index 7b10fc1212..e341f02901 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -7,7 +7,8 @@ namespace Avalonia.Rendering { public class RendererBase { - private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18); + private static readonly Typeface s_fpsTypeface = new Typeface("Arial"); + private static int s_fontSize = 18; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private int _framesThisSecond; private int _fps; @@ -18,7 +19,8 @@ namespace Avalonia.Rendering { _fpsText = new FormattedText { - Typeface = s_fpsTypeface + Typeface = s_fpsTypeface, + FontSize = s_fontSize }; } diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 4f884cdf33..68da513528 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -12,5 +12,6 @@ + diff --git a/src/Skia/Avalonia.Skia/FontKey.cs b/src/Skia/Avalonia.Skia/FontKey.cs new file mode 100644 index 0000000000..bb3fe230c1 --- /dev/null +++ b/src/Skia/Avalonia.Skia/FontKey.cs @@ -0,0 +1,40 @@ +// 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.Media; + +namespace Avalonia.Skia +{ + internal readonly struct FontKey : IEquatable + { + public readonly FontStyle Style; + public readonly FontWeight Weight; + + public FontKey(FontWeight weight, FontStyle style) + { + Style = style; + Weight = weight; + } + + public override int GetHashCode() + { + var hash = 17; + hash = hash * 31 + (int)Style; + hash = hash * 31 + (int)Weight; + + return hash; + } + + public override bool Equals(object other) + { + return other is FontKey key && Equals(key); + } + + public bool Equals(FontKey other) + { + return Style == other.Style && + Weight == other.Weight; + } + } +} diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs new file mode 100644 index 0000000000..6c67438533 --- /dev/null +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -0,0 +1,91 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + internal class FontManagerImpl : IFontManagerImpl + { + private SKFontManager _skFontManager = SKFontManager.Default; + + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); + + public FontManagerImpl() + { + DefaultFontFamilyName = SKTypeface.Default.FamilyName; + } + + public string DefaultFontFamilyName { get; } + + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + { + if (checkForUpdates) + { + _skFontManager = SKFontManager.CreateDefault(); + } + + return _skFontManager.FontFamilies; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + } + + public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + { + return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface; + } + + public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null) + { + var fontFamilyName = FontFamily.Default.Name; + + if (culture == null) + { + culture = CultureInfo.CurrentUICulture; + } + + if (fontFamily != null) + { + foreach (var familyName in fontFamily.FamilyNames) + { + var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight, + SKFontStyleWidth.Normal, + (SKFontStyleSlant)fontStyle, + new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint); + + if (skTypeface == null) + { + continue; + } + + fontFamilyName = familyName; + + break; + } + } + else + { + var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, + (SKFontStyleSlant)fontStyle, + new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint); + + if (skTypeface != null) + { + fontFamilyName = skTypeface.FamilyName; + } + } + + return GetTypeface(fontFamilyName, fontWeight, fontStyle); + } + } +} diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index eb7b65cdce..78ff785bdc 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -18,6 +18,7 @@ namespace Avalonia.Skia public FormattedTextImpl( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -28,47 +29,22 @@ namespace Avalonia.Skia // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); - SKTypeface skiaTypeface = null; + var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style); - if (typeface.FontFamily.Key != null) + _paint = new SKPaint { - var typefaces = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); - skiaTypeface = typefaces.GetTypeFace(typeface); - } - else - { - if (typeface.FontFamily.FamilyNames.HasFallbacks) - { - foreach (var familyName in typeface.FontFamily.FamilyNames) - { - skiaTypeface = TypefaceCache.GetTypeface( - familyName, - typeface.Style, - typeface.Weight); - if (skiaTypeface.FamilyName != TypefaceCache.DefaultFamilyName) break; - } - } - else - { - skiaTypeface = TypefaceCache.GetTypeface( - typeface.FontFamily.Name, - typeface.Style, - typeface.Weight); - } - } - - _paint = new SKPaint(); + TextEncoding = SKTextEncoding.Utf16, + IsStroke = false, + IsAntialias = true, + LcdRenderText = true, + SubpixelText = true, + Typeface = entry.SKTypeface, + TextSize = (float)fontSize, + TextAlign = textAlignment.ToSKTextAlign() + }; //currently Skia does not measure properly with Utf8 !!! //Paint.TextEncoding = SKTextEncoding.Utf8; - _paint.TextEncoding = SKTextEncoding.Utf16; - _paint.IsStroke = false; - _paint.IsAntialias = true; - _paint.LcdRenderText = true; - _paint.SubpixelText = true; - _paint.Typeface = skiaTypeface; - _paint.TextSize = (float)typeface.FontSize; - _paint.TextAlign = textAlignment.ToSKTextAlign(); _wrapping = wrapping; _constraint = constraint; diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs new file mode 100644 index 0000000000..b9b5b07d7d --- /dev/null +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -0,0 +1,179 @@ +// 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; +using Avalonia.Media; +using Avalonia.Platform; +using HarfBuzzSharp; +using SkiaSharp; + +namespace Avalonia.Skia +{ + internal class GlyphTypefaceImpl : IGlyphTypefaceImpl + { + private bool _isDisposed; + + public GlyphTypefaceImpl(Typeface typeface) + { + Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface; + + Face = new Face(GetTable) + { + UnitsPerEm = Typeface.UnitsPerEm + }; + + Font = new Font(Face); + + Font.SetFunctionsOpenType(); + + Font.GetScale(out var xScale, out _); + + DesignEmHeight = (short)xScale; + + if (!Font.TryGetHorizontalFontExtents(out var fontExtents)) + { + Font.TryGetVerticalFontExtents(out fontExtents); + } + + Ascent = -fontExtents.Ascender; + + Descent = -fontExtents.Descender; + + LineGap = fontExtents.LineGap; + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition)) + { + UnderlinePosition = underlinePosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness)) + { + UnderlineThickness = underlineThickness; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition)) + { + StrikethroughPosition = strikethroughPosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness)) + { + StrikethroughThickness = strikethroughThickness; + } + } + + public Face Face { get; } + + public Font Font { get; } + + public SKTypeface Typeface { get; } + + /// + public short DesignEmHeight { get; } + + /// + public int Ascent { get; } + + /// + public int Descent { get; } + + /// + public int LineGap { get; } + + //ToDo: Get these values from HarfBuzz + /// + public int UnderlinePosition { get; } + + /// + public int UnderlineThickness { get; } + + /// + public int StrikethroughPosition { get; } + + /// + public int StrikethroughThickness { get; } + + /// + public ushort GetGlyph(uint codepoint) + { + if (Font.TryGetGlyph(codepoint, out var glyph)) + { + return (ushort)glyph; + } + + return 0; + } + + /// + public ushort[] GetGlyphs(ReadOnlySpan codepoints) + { + var glyphs = new ushort[codepoints.Length]; + + for (var i = 0; i < codepoints.Length; i++) + { + if (Font.TryGetGlyph(codepoints[i], out var glyph)) + { + glyphs[i] = (ushort)glyph; + } + } + + return glyphs; + } + + /// + public int GetGlyphAdvance(ushort glyph) + { + return Font.GetHorizontalGlyphAdvance(glyph); + } + + /// + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) + { + var glyphIndices = new uint[glyphs.Length]; + + for (var i = 0; i < glyphs.Length; i++) + { + glyphIndices[i] = glyphs[i]; + } + + return Font.GetHorizontalGlyphAdvances(glyphIndices); + } + + private Blob GetTable(Face face, Tag tag) + { + var size = Typeface.GetTableSize(tag); + + var data = Marshal.AllocCoTaskMem(size); + + var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data)); + + return Typeface.TryGetTableData(tag, 0, size, data) ? + new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null; + } + + private void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!disposing) + { + return; + } + + Font?.Dispose(); + Face?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 15f38b1c4f..e4ad0c1b24 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -21,8 +21,6 @@ namespace Avalonia.Skia private GRContext GrContext { get; } - public IEnumerable InstalledFontNames => SKFontManager.Default.FontFamilies; - public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu) { if (customSkiaGpu != null) @@ -52,12 +50,13 @@ namespace Avalonia.Skia public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList spans) { - return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); + return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans); } public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 17448127b0..8ec2a9c3f8 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -4,114 +4,59 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; - using Avalonia.Media; -using SkiaSharp; - namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary> _fontFamilies = - new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> _fontFamilies = + new ConcurrentDictionary>(); - public void AddTypeFace(SKTypeface typeface) + public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry) { - var key = new FontKey((SKFontStyleWeight)typeface.FontWeight, typeface.FontSlant); - - if (!_fontFamilies.TryGetValue(typeface.FamilyName, out var fontFamily)) + if (!_fontFamilies.TryGetValue(familyName, out var fontFamily)) { - fontFamily = new ConcurrentDictionary(); + fontFamily = new ConcurrentDictionary(); - _fontFamilies.TryAdd(typeface.FamilyName, fontFamily); + _fontFamilies.TryAdd(familyName, fontFamily); } - fontFamily.TryAdd(key, typeface); + fontFamily.TryAdd(key, entry); } - public SKTypeface GetTypeFace(Typeface typeface) + public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle) { - var styleSlant = SKFontStyleSlant.Upright; - - switch (typeface.Style) - { - case FontStyle.Italic: - styleSlant = SKFontStyleSlant.Italic; - break; - - case FontStyle.Oblique: - styleSlant = SKFontStyleSlant.Oblique; - break; - } + var key = new FontKey(fontWeight, fontStyle); - if (!_fontFamilies.TryGetValue(typeface.FontFamily.Name, out var fontFamily)) - { - return TypefaceCache.GetTypeface(TypefaceCache.DefaultFamilyName, typeface.Style, typeface.Weight); - } - - var weight = (SKFontStyleWeight)typeface.Weight; - - var key = new FontKey(weight, styleSlant); - - return fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)); + return _fontFamilies.TryGetValue(familyName, out var fontFamily) ? + fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) : + null; } - private static SKTypeface GetFallback(IDictionary fontFamily, FontKey key) + private static TypefaceCollectionEntry GetFallback(IDictionary fontFamily, FontKey key) { var keys = fontFamily.Keys.Where( - x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Slant == key.Slant).ToArray(); + x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray(); if (!keys.Any()) { keys = fontFamily.Keys.Where( - x => x.Weight == key.Weight && (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); + x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); if (!keys.Any()) { keys = fontFamily.Keys.Where( x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && - (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); + (x.Style >= key.Style || x.Style < key.Style)).ToArray(); } } key = keys.FirstOrDefault(); - fontFamily.TryGetValue(key, out var typeface); - - return typeface; - } - - private struct FontKey - { - public readonly SKFontStyleSlant Slant; - public readonly SKFontStyleWeight Weight; - - public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant) - { - Slant = slant; - Weight = weight; - } - - public override int GetHashCode() - { - var hash = 17; - hash = (hash * 31) + (int)Slant; - hash = (hash * 31) + (int)Weight; - - return hash; - } + fontFamily.TryGetValue(key, out var entry); - public override bool Equals(object other) - { - return other is FontKey key && this.Equals(key); - } - - private bool Equals(FontKey other) - { - return Slant == other.Slant && - Weight == other.Weight; - } + return entry; } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index ab8ee85a54..4bb42c7118 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -45,9 +45,13 @@ namespace Avalonia.Skia { var assetStream = assetLoader.Open(asset); - var typeface = SKTypeface.FromStream(assetStream); + var skTypeface = SKTypeface.FromStream(assetStream); - typeFaceCollection.AddTypeFace(typeface); + var typeface = new Typeface(fontFamily, (FontWeight)skTypeface.FontWeight, (FontStyle)skTypeface.FontSlant); + + var entry = new TypefaceCollectionEntry(typeface, skTypeface); + + typeFaceCollection.AddEntry(skTypeface.FamilyName, new FontKey(typeface.Weight, typeface.Style), entry); } return typeFaceCollection; diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index f16e967f42..ce3aef755b 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -25,6 +25,11 @@ namespace Avalonia.Skia AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface); + + var fontManager = new FontManagerImpl(); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(fontManager); } /// diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs index 9e270114d2..1c2b855032 100644 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ b/src/Skia/Avalonia.Skia/TypefaceCache.cs @@ -1,7 +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.Collections.Generic; +using System.Collections.Concurrent; using Avalonia.Media; using SkiaSharp; @@ -12,88 +12,36 @@ namespace Avalonia.Skia /// internal static class TypefaceCache { - public static readonly string DefaultFamilyName = CreateDefaultFamilyName(); + private static readonly ConcurrentDictionary> s_cache = + new ConcurrentDictionary>(); - private static readonly Dictionary> s_cache = - new Dictionary>(); - - struct FontKey + public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { - public readonly SKFontStyleSlant Slant; - public readonly SKFontStyleWeight Weight; - - public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant) + if (fontFamily.Key != null) { - Slant = slant; - Weight = weight; + return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily) + .Get(fontFamily.Name, fontWeight, fontStyle); } - public override int GetHashCode() - { - int hash = 17; - hash = hash * 31 + (int)Slant; - hash = hash * 31 + (int)Weight; - - return hash; - } - - public override bool Equals(object other) - { - return other is FontKey ? Equals((FontKey)other) : false; - } - - public bool Equals(FontKey other) - { - return Slant == other.Slant && - Weight == other.Weight; - } - - // Equals and GetHashCode ommitted - } - - private static string CreateDefaultFamilyName() - { - var defaultTypeface = SKTypeface.CreateDefault(); + var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary()); - return defaultTypeface.FamilyName; - } + var key = new FontKey(fontWeight, fontStyle); - private static SKTypeface GetTypeface(string name, FontKey key) - { - var familyKey = name; - - if (!s_cache.TryGetValue(familyKey, out var entry)) + if (typefaceCollection.TryGetValue(key, out var entry)) { - s_cache[familyKey] = entry = new Dictionary(); + return entry; } - if (!entry.TryGetValue(key, out var typeface)) - { - typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant) ?? - GetTypeface(DefaultFamilyName, key); + var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight, + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default; - entry[key] = typeface; - } + var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle); - return typeface; - } - - public static SKTypeface GetTypeface(string name, FontStyle style, FontWeight weight) - { - var skStyle = SKFontStyleSlant.Upright; + entry = new TypefaceCollectionEntry(typeface, skTypeface); - switch (style) - { - case FontStyle.Italic: - skStyle = SKFontStyleSlant.Italic; - break; - - case FontStyle.Oblique: - skStyle = SKFontStyleSlant.Oblique; - break; - } + typefaceCollection[key] = entry; - return GetTypeface(name, new FontKey((SKFontStyleWeight)weight, skStyle)); + return entry; } } } diff --git a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs b/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs new file mode 100644 index 0000000000..ef9f889819 --- /dev/null +++ b/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs @@ -0,0 +1,19 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Media; +using SkiaSharp; + +namespace Avalonia.Skia +{ + internal class TypefaceCollectionEntry + { + public TypefaceCollectionEntry(Typeface typeface, SKTypeface skTypeface) + { + Typeface = typeface; + SKTypeface = skTypeface; + } + public Typeface Typeface { get; } + public SKTypeface SKTypeface { get; } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index 458d8f9cbb..7d47b95ede 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 5ab9a8f74d..e76596e925 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -41,20 +41,6 @@ namespace Avalonia.Direct2D1 public static SharpDX.DXGI.Device1 DxgiDevice { get; private set; } - public IEnumerable InstalledFontNames - { - get - { - var cache = Direct2D1FontCollectionCache.s_installedFontCollection; - var length = cache.FontFamilyCount; - for (int i = 0; i < length; i++) - { - var names = cache.GetFontFamily(i).FamilyNames; - yield return names.GetString(0); - } - } - } - private static readonly object s_initLock = new object(); private static bool s_initialized = false; @@ -120,6 +106,7 @@ namespace Avalonia.Direct2D1 { InitializeDirect2D(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new FontManagerImpl()); SharpDX.Configuration.EnableReleaseOnFinalizer = true; } @@ -131,6 +118,7 @@ namespace Avalonia.Direct2D1 public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -139,6 +127,7 @@ namespace Avalonia.Direct2D1 return new FormattedTextImpl( text, typeface, + fontSize, textAlignment, wrapping, constraint, diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index d93a59d384..b455c4fbee 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -1,62 +1,61 @@ using System.Collections.Concurrent; using Avalonia.Media; using Avalonia.Media.Fonts; +using SharpDX.DirectWrite; +using FontFamily = Avalonia.Media.FontFamily; +using FontStyle = SharpDX.DirectWrite.FontStyle; +using FontWeight = SharpDX.DirectWrite.FontWeight; namespace Avalonia.Direct2D1.Media { internal static class Direct2D1FontCollectionCache { - private static readonly ConcurrentDictionary s_cachedCollections; - internal static readonly SharpDX.DirectWrite.FontCollection s_installedFontCollection; + private static readonly ConcurrentDictionary s_cachedCollections; + internal static readonly FontCollection InstalledFontCollection; static Direct2D1FontCollectionCache() { - s_cachedCollections = new ConcurrentDictionary(); + s_cachedCollections = new ConcurrentDictionary(); - s_installedFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false); + InstalledFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false); } - public static SharpDX.DirectWrite.TextFormat GetTextFormat(Typeface typeface) + public static Font GetFont(Typeface typeface) { var fontFamily = typeface.FontFamily; var fontCollection = GetOrAddFontCollection(fontFamily); - var fontFamilyName = FontFamily.Default.Name; - // Should this be cached? foreach (var familyName in fontFamily.FamilyNames) { - if (!fontCollection.FindFamilyName(familyName, out _)) + if (fontCollection.FindFamilyName(familyName, out var index)) { - continue; + return fontCollection.GetFontFamily(index).GetFirstMatchingFont( + (FontWeight)typeface.Weight, + FontStretch.Normal, + (FontStyle)typeface.Style); } - - fontFamilyName = familyName; - - break; } - return new SharpDX.DirectWrite.TextFormat( - Direct2D1Platform.DirectWriteFactory, - fontFamilyName, - fontCollection, - (SharpDX.DirectWrite.FontWeight)typeface.Weight, - (SharpDX.DirectWrite.FontStyle)typeface.Style, - SharpDX.DirectWrite.FontStretch.Normal, - (float)typeface.FontSize); + InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i); + + return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont( + (FontWeight)typeface.Weight, + FontStretch.Normal, + (FontStyle)typeface.Style); } - private static SharpDX.DirectWrite.FontCollection GetOrAddFontCollection(FontFamily fontFamily) + private static FontCollection GetOrAddFontCollection(FontFamily fontFamily) { - return fontFamily.Key == null ? s_installedFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection); + return fontFamily.Key == null ? InstalledFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection); } - private static SharpDX.DirectWrite.FontCollection CreateFontCollection(FontFamilyKey key) + private static FontCollection CreateFontCollection(FontFamilyKey key) { var assets = FontFamilyLoader.LoadFontAssets(key); var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets); - return new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); + return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs new file mode 100644 index 0000000000..de1a4cf2d1 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.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.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Platform; +using SharpDX.DirectWrite; +using FontFamily = Avalonia.Media.FontFamily; +using FontStyle = Avalonia.Media.FontStyle; +using FontWeight = Avalonia.Media.FontWeight; + +namespace Avalonia.Direct2D1.Media +{ + internal class FontManagerImpl : IFontManagerImpl + { + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); + + public FontManagerImpl() + { + //ToDo: Implement a real lookup of the system's default font. + DefaultFontFamilyName = "segoe ui"; + } + + public string DefaultFontFamilyName { get; } + + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + { + var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; + + var fontFamilies = new string[familyCount]; + + for (var i = 0; i < familyCount; i++) + { + fontFamilies[i] = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i).FamilyNames.GetString(0); + } + + return fontFamilies; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + } + + public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + { + //ToDo: Implement caching. + return new Typeface(fontFamily, fontWeight, fontStyle); + } + + public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null) + { + var fontFamilyName = FontFamily.Default.Name; + + var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; + + for (var i = 0; i < familyCount; i++) + { + var font = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i) + .GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, FontStretch.Normal, + (SharpDX.DirectWrite.FontStyle)fontStyle).GetFont(0); + + if (!font.HasCharacter(codepoint)) + { + continue; + } + + fontFamilyName = font.FontFamily.FamilyNames.GetString(0); + + break; + } + + return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index b73deb1f0a..b1a177ad24 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -14,6 +14,7 @@ namespace Avalonia.Direct2D1.Media public FormattedTextImpl( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, @@ -21,20 +22,20 @@ namespace Avalonia.Direct2D1.Media { Text = text; - using (var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface)) + using (var font = Direct2D1FontCollectionCache.GetFont(typeface)) + using (var textFormat = new DWrite.TextFormat(Direct2D1Platform.DirectWriteFactory, + typeface.FontFamily.Name, font.FontFamily.FontCollection, (DWrite.FontWeight)typeface.Weight, + (DWrite.FontStyle)typeface.Style, DWrite.FontStretch.Normal, (float)fontSize)) { textFormat.WordWrapping = wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; TextLayout = new DWrite.TextLayout( - Direct2D1Platform.DirectWriteFactory, - Text ?? string.Empty, - textFormat, - (float)constraint.Width, - (float)constraint.Height) - { - TextAlignment = textAlignment.ToDirect2D() - }; + Direct2D1Platform.DirectWriteFactory, + Text ?? string.Empty, + textFormat, + (float)constraint.Width, + (float)constraint.Height) { TextAlignment = textAlignment.ToDirect2D() }; } if (spans != null) diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs new file mode 100644 index 0000000000..66cf397110 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs @@ -0,0 +1,188 @@ +// 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.Media; +using Avalonia.Platform; +using HarfBuzzSharp; +using SharpDX.DirectWrite; + +namespace Avalonia.Direct2D1.Media +{ + internal class GlyphTypefaceImpl : IGlyphTypefaceImpl + { + private bool _isDisposed; + + public GlyphTypefaceImpl(Typeface typeface) + { + DWFont = Direct2D1FontCollectionCache.GetFont(typeface); + + FontFace = new FontFace(DWFont); + + Face = new Face(GetTable); + + Font = new HarfBuzzSharp.Font(Face); + + Font.SetFunctionsOpenType(); + + Font.GetScale(out var xScale, out _); + + DesignEmHeight = (short)xScale; + + if (!Font.TryGetHorizontalFontExtents(out var fontExtents)) + { + Font.TryGetVerticalFontExtents(out fontExtents); + } + + Ascent = -fontExtents.Ascender; + + Descent = -fontExtents.Descender; + + LineGap = fontExtents.LineGap; + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition)) + { + UnderlinePosition = underlinePosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness)) + { + UnderlineThickness = underlineThickness; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition)) + { + StrikethroughPosition = strikethroughPosition; + } + + if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness)) + { + StrikethroughThickness = strikethroughThickness; + } + } + + private Blob GetTable(Face face, Tag tag) + { + var dwTag = (int)SwapBytes(tag); + + if (FontFace.TryGetFontTable(dwTag, out var tableData, out _)) + { + return new Blob(tableData.Pointer, tableData.Size, MemoryMode.ReadOnly, () => { }); + } + + return null; + } + + private static uint SwapBytes(uint x) + { + x = (x >> 16) | (x << 16); + + return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8); + } + + public SharpDX.DirectWrite.Font DWFont { get; } + + public FontFace FontFace { get; } + + public Face Face { get; } + + public HarfBuzzSharp.Font Font { get; } + + /// + public short DesignEmHeight { get; } + + /// + public int Ascent { get; } + + /// + public int Descent { get; } + + /// + public int LineGap { get; } + + //ToDo: Read font table for these values + /// + public int UnderlinePosition { get; } + + /// + public int UnderlineThickness { get; } + + /// + public int StrikethroughPosition { get; } + + /// + public int StrikethroughThickness { get; } + + /// + public ushort GetGlyph(uint codepoint) + { + if (Font.TryGetGlyph(codepoint, out var glyph)) + { + return (ushort)glyph; + } + + return 0; + } + + /// + public ushort[] GetGlyphs(ReadOnlySpan codepoints) + { + var glyphs = new ushort[codepoints.Length]; + + for (var i = 0; i < codepoints.Length; i++) + { + if (Font.TryGetGlyph(codepoints[i], out var glyph)) + { + glyphs[i] = (ushort)glyph; + } + } + + return glyphs; + } + + /// + public int GetGlyphAdvance(ushort glyph) + { + return Font.GetHorizontalGlyphAdvance(glyph); + } + + /// + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) + { + var glyphIndices = new uint[glyphs.Length]; + + for (var i = 0; i < glyphs.Length; i++) + { + glyphIndices[i] = glyphs[i]; + } + + return Font.GetHorizontalGlyphAdvances(glyphIndices); + } + + private void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!disposing) + { + return; + } + + Font?.Dispose(); + Face?.Dispose(); + FontFace?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} + diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index 6cf38b6121..a683e5cfca 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -175,6 +175,7 @@ namespace Avalonia.Layout.UnitTests x.CreateFormattedText( It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index 353123ab2a..bca34dd69d 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -53,7 +53,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var r = AvaloniaLocator.Current.GetService(); return r.CreateFormattedText(text, - new Typeface(fontFamily, fontSize, fontStyle, fontWeight), + new Typeface(fontFamily, fontWeight, fontStyle), + fontSize, textAlignment, wrapping, widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity), diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index a3cc3dec17..b3f0af55f4 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -9,11 +9,10 @@ namespace Avalonia.UnitTests { public class MockPlatformRenderInterface : IPlatformRenderInterface { - public IEnumerable InstalledFontNames => new string[0]; - public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index f7a878feba..d189aa3165 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -169,6 +169,7 @@ namespace Avalonia.UnitTests x.CreateFormattedText( It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs index 75ae43a1fa..5d47333d51 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs @@ -19,12 +19,48 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(new FontFamily("Arial"), fontFamily); } - [Fact] - public void Should_Be_Equal() + [InlineData("Font A")] + [InlineData("Font A, Font B")] + [InlineData("resm: Avalonia.Visuals.UnitTests#MyFont")] + [InlineData("avares://Avalonia.Visuals.UnitTests/Assets/Fonts#MyFont")] + [Theory] + public void Should_Have_Equal_Hash(string s) { - var fontFamily = new FontFamily("Arial"); + var fontFamily = new FontFamily(s); - Assert.Equal(new FontFamily("Arial"), fontFamily); + Assert.Equal(new FontFamily(s).GetHashCode(), fontFamily.GetHashCode()); + } + + [InlineData("Font A, Font B", "Font B, Font A")] + [InlineData("Font A, Font B", "Font A, Font C")] + [Theory] + public void Should_Not_Have_Equal_Hash(string a, string b) + { + var fontFamily = new FontFamily(b); + + Assert.NotEqual(new FontFamily(a).GetHashCode(), fontFamily.GetHashCode()); + } + + [InlineData("Font A")] + [InlineData("Font A, Font B")] + [InlineData("resm: Avalonia.Visuals.UnitTests#MyFont")] + [InlineData("avares://Avalonia.Visuals.UnitTests/Assets/Fonts#MyFont")] + [Theory] + public void Should_Be_Equal(string s) + { + var fontFamily = new FontFamily(s); + + Assert.Equal(new FontFamily(s), fontFamily); + } + + [InlineData("Font A, Font B", "Font B, Font A")] + [InlineData("Font A, Font B", "Font A, Font C")] + [Theory] + public void Should_Not_Be_Equal(string a, string b) + { + var fontFamily = new FontFamily(b); + + Assert.NotEqual(new FontFamily(a), fontFamily); } [Fact] diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs index 2c8f8eb9b2..0e43c76da1 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs @@ -7,15 +7,21 @@ namespace Avalonia.Visuals.UnitTests.Media public class TypefaceTests { [Fact] - public void Exception_Should_Be_Thrown_If_FontSize_LessThanEqualTo_0() + public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_Zero() { - Assert.Throws(() => new Typeface("foo", 0)); + Assert.Throws(() => new Typeface("foo", 0, (FontStyle)12)); } [Fact] - public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_0() + public void Should_Be_Equal() { - Assert.Throws(() => new Typeface("foo", 12, weight: 0)); + Assert.Equal(new Typeface("Font A"), new Typeface("Font A")); + } + + [Fact] + public void Should_Have_Equal_Hash() + { + Assert.Equal(new Typeface("Font A").GetHashCode(), new Typeface("Font A").GetHashCode()); } } } diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index d31210bc71..335cdf4597 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -13,6 +13,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, + double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, From 9d105c7dbbb026cb7d3d362f96aa24cb70e3e9d2 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Nov 2019 12:01:02 +0100 Subject: [PATCH 054/126] Introduce a static FontManager that uses a platform implementation under the hood. --- src/Avalonia.Visuals/Media/FontFamily.cs | 7 +- src/Avalonia.Visuals/Media/FontManager.cs | 92 +++++-------------- src/Avalonia.Visuals/Media/GlyphTypeface.cs | 5 +- .../Platform/IFontManagerImpl.cs | 9 -- .../Platform/IPlatformRenderInterface.cs | 9 ++ src/Skia/Avalonia.Skia/FontManagerImpl.cs | 9 -- .../Avalonia.Skia/PlatformRenderInterface.cs | 9 ++ .../Avalonia.Direct2D1/Direct2D1Platform.cs | 8 ++ .../Media/FontManagerImpl.cs | 9 -- 9 files changed, 59 insertions(+), 98 deletions(-) diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index 665dfc1129..d263097e6a 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -12,7 +12,7 @@ namespace Avalonia.Media { static FontFamily() { - Default = new FontFamily(FontManager.Default.DefaultFontFamilyName); + Default = new FontFamily(FontManager.DefaultFontFamilyName); } /// @@ -60,8 +60,11 @@ namespace Avalonia.Media /// /// Represents all font families in the system. This can be an expensive call depending on platform implementation. /// + /// + /// Consider using the new instead. + /// public static IEnumerable SystemFontFamilies => - FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); + FontManager.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); /// /// Gets the primary family name of the font family. diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index e89471ede8..95d91e7df1 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -1,99 +1,55 @@ // 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.Globalization; using Avalonia.Platform; namespace Avalonia.Media { - public abstract class FontManager : IFontManagerImpl + public static class FontManager { - public static readonly FontManager Default = CreateDefaultFontManger(); + private static readonly IFontManagerImpl s_platformImpl = GetPlatformImpl(); - /// - public string DefaultFontFamilyName { get; protected set; } + /// + public static string DefaultFontFamilyName => s_platformImpl.DefaultFontFamilyName; - private static FontManager CreateDefaultFontManger() - { - var platformImpl = AvaloniaLocator.Current.GetService(); - - if(platformImpl == null) - { - return new EmptyFontManager(); - } - - return new PlatformFontManger(platformImpl); - } + /// + public static IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + s_platformImpl.GetInstalledFontFamilyNames(checkForUpdates); - /// - public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + /// + public static Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => + s_platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle); - /// - public abstract IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); - - /// - public abstract Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); - - /// - public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, + /// + public static Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null); + FontFamily fontFamily = null, CultureInfo culture = null) => + s_platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture); - private class PlatformFontManger : FontManager + private static IFontManagerImpl GetPlatformImpl() { - private readonly IFontManagerImpl _platformImpl; - - public PlatformFontManger(IFontManagerImpl platformImpl) - { - _platformImpl = platformImpl; - - DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName; - } - - public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - _platformImpl.GetInstalledFontFamilyNames(checkForUpdates); - - public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) => _platformImpl.CreateGlyphTypeface(typeface); - - public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => - _platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle); + var platformImpl = AvaloniaLocator.Current.GetService(); - public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, - FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) => - _platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture); + return platformImpl ?? new EmptyFontManagerImpl(); } - private class EmptyFontManager : FontManager + private class EmptyFontManagerImpl : IFontManagerImpl { - private readonly string[] _defaultFontFamilies = { "Arial" }; - - public EmptyFontManager() - { - DefaultFontFamilyName = "Arial"; - } - - public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) - { - return _defaultFontFamilies; - } + public string DefaultFontFamilyName => "Arial"; - public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) - { - throw new NotSupportedException(); - } + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => new[] { "Arial" }; - public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { - throw new NotSupportedException(); + return new Typeface(fontFamily, fontWeight, fontStyle); } - public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, + public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, FontFamily fontFamily = null, CultureInfo culture = null) { - throw new NotSupportedException(); + return null; } } } diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs index 1c959a86c5..cba7c8c795 100644 --- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -9,7 +9,10 @@ namespace Avalonia.Media { public class GlyphTypeface : IDisposable { - public GlyphTypeface(Typeface typeface) : this(FontManager.Default.CreateGlyphTypeface(typeface)) + private static readonly IPlatformRenderInterface s_platformRenderInterface = + AvaloniaLocator.Current.GetService(); + + public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface)) { } diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 236631edde..254b5d07d1 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -20,15 +20,6 @@ namespace Avalonia.Platform /// IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); - /// - /// Creates a glyph typeface for specified typeface. - /// - /// The typeface. - /// - /// The glyph typeface implementation. - /// - IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); - /// /// Get a typeface from specified parameters. /// diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 619d3088b4..5a0a7b2f19 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -111,5 +111,14 @@ namespace Avalonia.Platform /// The number of bytes per row. /// An . IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride); + + /// + /// Creates a glyph typeface for specified typeface. + /// + /// The typeface. + /// + /// The glyph typeface implementation. + /// + IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); } } diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 6c67438533..03de82178a 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using Avalonia.Media; @@ -14,9 +13,6 @@ namespace Avalonia.Skia { private SKFontManager _skFontManager = SKFontManager.Default; - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - public FontManagerImpl() { DefaultFontFamilyName = SKTypeface.Default.FamilyName; @@ -34,11 +30,6 @@ namespace Avalonia.Skia return _skFontManager.FontFamilies; } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) - { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); - } - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index e4ad0c1b24..ee0cfb2f06 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.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.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls.Platform.Surfaces; @@ -17,6 +18,9 @@ namespace Avalonia.Skia /// internal class PlatformRenderInterface : IPlatformRenderInterface { + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); + private readonly ICustomSkiaGpu _customSkiaGpu; private GRContext GrContext { get; } @@ -150,5 +154,10 @@ namespace Avalonia.Skia { return new WriteableBitmapImpl(size, dpi, format); } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index e76596e925..1bda5157a5 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.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.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls; @@ -27,6 +28,8 @@ namespace Avalonia.Direct2D1 { public class Direct2D1Platform : IPlatformRenderInterface { + private readonly ConcurrentDictionary _glyphTypefaceCache = + new ConcurrentDictionary(); private static readonly Direct2D1Platform s_instance = new Direct2D1Platform(); public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; } @@ -190,5 +193,10 @@ namespace Avalonia.Direct2D1 { return new WicBitmapImpl(format, data, size, dpi, stride); } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index de1a4cf2d1..94de397652 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using Avalonia.Media; @@ -15,9 +14,6 @@ namespace Avalonia.Direct2D1.Media { internal class FontManagerImpl : IFontManagerImpl { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - public FontManagerImpl() { //ToDo: Implement a real lookup of the system's default font. @@ -40,11 +36,6 @@ namespace Avalonia.Direct2D1.Media return fontFamilies; } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) - { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); - } - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { //ToDo: Implement caching. From 46d3a916527e791c0c16ba1ec99b39d828ffbd1a Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Nov 2019 12:08:54 +0100 Subject: [PATCH 055/126] Add missing mocks --- tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs | 5 +++++ .../VisualTree/MockRenderInterface.cs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index b3f0af55f4..187853283f 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -78,5 +78,10 @@ namespace Avalonia.UnitTests { throw new NotImplementedException(); } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return Mock.Of(); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 335cdf4597..032b6582a9 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -52,6 +52,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + throw new NotImplementedException(); + } + public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt) { throw new NotImplementedException(); From 38ae0140c253475d9695474e75587382f56969f3 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Nov 2019 17:03:12 +0100 Subject: [PATCH 056/126] Remove special case that checks for AvaloniaList --- src/Avalonia.Controls/TabControl.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 87ba282db0..8b1cd48379 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -4,7 +4,6 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; -using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -180,14 +179,6 @@ namespace Avalonia.Controls var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index); - if (container == null) - { - if (Items is AvaloniaList items) - { - container = items[index] as TabItem; - } - } - if (container == null) { return; @@ -196,7 +187,7 @@ namespace Avalonia.Controls UpdateSelectedContent(container); } - private void UpdateSelectedContent(TabItem item) + private void UpdateSelectedContent(IContentControl item) { if (SelectedContentTemplate != item.ContentTemplate) { From a612ee5a3eb1f5d80dfd540e5c64f5d998139784 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 1 Nov 2019 18:04:12 +0200 Subject: [PATCH 057/126] fix WrapPanel issue #3165 item width/height should trigger new measure --- src/Avalonia.Controls/WrapPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 7c88401615..d06a71a9f8 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls /// static WrapPanel() { - AffectsMeasure(OrientationProperty); + AffectsMeasure(OrientationProperty, ItemWidthProperty, ItemHeightProperty); } /// From e1df65c94b4b829b87403e472da0d8931ce5cddd Mon Sep 17 00:00:00 2001 From: mstr2 Date: Fri, 1 Nov 2019 18:36:25 +0100 Subject: [PATCH 058/126] Failing unit test for #3197 --- .../Properties/AssemblyInfo.cs | 1 + .../TransitionsTests.cs | 28 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Animation/Properties/AssemblyInfo.cs b/src/Avalonia.Animation/Properties/AssemblyInfo.cs index eb38a66a84..8523b9537d 100644 --- a/src/Avalonia.Animation/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Animation/Properties/AssemblyInfo.cs @@ -10,3 +10,4 @@ using System.Runtime.CompilerServices; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")] [assembly: InternalsVisibleTo("Avalonia.LeakTests")] +[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests")] diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs index f1b4b0d071..22f3b4f501 100644 --- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs +++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs @@ -1,14 +1,7 @@ using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Animation; using Avalonia.Controls; -using Avalonia.Styling; using Avalonia.UnitTests; -using Avalonia.Data; using Xunit; -using Avalonia.Animation.Easings; namespace Avalonia.Animation.UnitTests { @@ -69,5 +62,26 @@ namespace Avalonia.Animation.UnitTests Assert.Equal(0, border.Opacity); } } + + [Fact] + public void TransitionInstance_With_Zero_Duration_Is_Completed_On_First_Tick() + { + var clock = new MockGlobalClock(); + + using (UnitTestApplication.Start(new TestServices(globalClock: clock))) + { + int i = 0; + var inst = new TransitionInstance(clock, TimeSpan.Zero).Subscribe(nextValue => + { + switch (i++) + { + case 0: Assert.Equal(0, nextValue); break; + case 1: Assert.Equal(1d, nextValue); break; + } + }); + + clock.Pulse(TimeSpan.FromMilliseconds(10)); + } + } } } From 533c17a1816b8eb7b8cc38670634e3f9690d264a Mon Sep 17 00:00:00 2001 From: mstr2 Date: Fri, 1 Nov 2019 18:37:18 +0100 Subject: [PATCH 059/126] TransitionInstance with zero duration is now completed on first tick --- src/Avalonia.Animation/TransitionInstance.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index 10ea6bf523..a69ad50a4b 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -28,7 +28,7 @@ namespace Avalonia.Animation private void TimerTick(TimeSpan t) { - var interpVal = (double)t.Ticks / _duration.Ticks; + var interpVal = _duration.Ticks == 0 ? 1d : (double)t.Ticks / _duration.Ticks; // Clamp interpolation value. if (interpVal >= 1d | interpVal < 0d) From 75142ce5fa71fc89991eeb4753352b50a6648eec Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 2 Nov 2019 21:43:01 +0100 Subject: [PATCH 060/126] Set grid splitter min width and height to avoid invisible splitters. Make splitter background lighter. --- src/Avalonia.Diagnostics/Views/TreePageView.xaml | 4 ++-- src/Avalonia.Themes.Default/GridSplitter.xaml | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Views/TreePageView.xaml index ca7314264a..2619fd744a 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Views/TreePageView.xaml @@ -2,7 +2,7 @@ xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Avalonia.Diagnostics.Views.TreePageView"> - + - + diff --git a/src/Avalonia.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/GridSplitter.xaml index cfab5dab56..dc5cd002dc 100644 --- a/src/Avalonia.Themes.Default/GridSplitter.xaml +++ b/src/Avalonia.Themes.Default/GridSplitter.xaml @@ -2,7 +2,9 @@ - - - - - - - - - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 4fc63ea054..7adceddacf 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -13,15 +13,6 @@ namespace ControlCatalog public override void Initialize() { AvaloniaXamlLoader.Load(this); - - Name = "Avalonia"; - - _recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu; - } - - public void OnOpenClicked(object sender, EventArgs args) - { - _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1))); } public override void OnFrameworkInitializationCompleted() From 3e8138cfe4782e5ca007f96abb5db41083a43d76 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Nov 2019 11:38:21 +0000 Subject: [PATCH 090/126] Set a default Avalonia application name. --- src/Avalonia.Controls/Application.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index ce60a0f0b9..59c6c47ed9 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -48,6 +48,14 @@ namespace Avalonia /// public event EventHandler ResourcesChanged; + /// + /// Creates an instance of the class. + /// + public Application() + { + Name = "Avalonia Application"; + } + /// /// Gets the current instance of the class. /// From 48234ad3ab8aea1c8ff5801ff20c4ea565f2e615 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 6 Nov 2019 20:21:15 +0800 Subject: [PATCH 091/126] Add a Avalonia about window default implementation. --- src/Avalonia.Dialogs/DefaultAboutWindow.xaml | 109 ++++++++++++++++++ .../DefaultAboutWindow.xaml.cs | 16 +++ 2 files changed, 125 insertions(+) create mode 100644 src/Avalonia.Dialogs/DefaultAboutWindow.xaml create mode 100644 src/Avalonia.Dialogs/DefaultAboutWindow.xaml.cs diff --git a/src/Avalonia.Dialogs/DefaultAboutWindow.xaml b/src/Avalonia.Dialogs/DefaultAboutWindow.xaml new file mode 100644 index 0000000000..8d666f7e54 --- /dev/null +++ b/src/Avalonia.Dialogs/DefaultAboutWindow.xaml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Dialogs/DefaultAboutWindow.xaml.cs b/src/Avalonia.Dialogs/DefaultAboutWindow.xaml.cs new file mode 100644 index 0000000000..b5adab5b7c --- /dev/null +++ b/src/Avalonia.Dialogs/DefaultAboutWindow.xaml.cs @@ -0,0 +1,16 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Dialogs +{ + public class DefaultAboutWindow : Window + { + public DefaultAboutWindow() + { + AvaloniaXamlLoader.Load(this); + } + } +} From 5da9bcd72304537e9f4eb6978775f6a1bfde729f Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 6 Nov 2019 22:51:47 +0800 Subject: [PATCH 092/126] Change to AboutAvaloniaDialog --- .../{DefaultAboutWindow.xaml => AboutAvaloniaDialog.xaml} | 2 +- ...DefaultAboutWindow.xaml.cs => AboutAvaloniaDialog.xaml.cs} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Avalonia.Dialogs/{DefaultAboutWindow.xaml => AboutAvaloniaDialog.xaml} (98%) rename src/Avalonia.Dialogs/{DefaultAboutWindow.xaml.cs => AboutAvaloniaDialog.xaml.cs} (79%) diff --git a/src/Avalonia.Dialogs/DefaultAboutWindow.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml similarity index 98% rename from src/Avalonia.Dialogs/DefaultAboutWindow.xaml rename to src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml index 8d666f7e54..fd62bddd3c 100644 --- a/src/Avalonia.Dialogs/DefaultAboutWindow.xaml +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml @@ -6,7 +6,7 @@ MinHeight="220" Title="About Avalonia" Background="Purple" - x:Class="Avalonia.Dialogs.DefaultAboutWindow"> + x:Class="Avalonia.Dialogs.AboutAvaloniaDialog"> - + + + + + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - + - + \ No newline at end of file From 08bb2399f6944c6fbcc444b1582a932a289641b1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 7 Nov 2019 13:41:38 +0000 Subject: [PATCH 101/126] implement File -> Exit and Help -> About in control catalog. --- samples/ControlCatalog/MainWindow.xaml | 4 ++-- .../ViewModels/MainWindowViewModel.cs | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index c45548e439..248f94082d 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -45,10 +45,10 @@ - + - + diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index adf0345a70..89e7653618 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,7 @@ using System.Reactive; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; +using Avalonia.Dialogs; using ReactiveUI; namespace ControlCatalog.ViewModels @@ -26,6 +28,20 @@ namespace ControlCatalog.ViewModels { NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "Native Notifications are not quite ready. Coming soon.", NotificationType.Error)); }); + + AboutCommand = ReactiveCommand.CreateFromTask(async () => + { + var dialog = new AboutAvaloniaDialog(); + + var mainWindow = (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow; + + await dialog.ShowDialog(mainWindow); + }); + + ExitCommand = ReactiveCommand.Create(() => + { + (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown(); + }); } public IManagedNotificationManager NotificationManager @@ -39,5 +55,9 @@ namespace ControlCatalog.ViewModels public ReactiveCommand ShowManagedNotificationCommand { get; } public ReactiveCommand ShowNativeNotificationCommand { get; } + + public ReactiveCommand AboutCommand { get; } + + public ReactiveCommand ExitCommand { get; } } } From 868b125848f2380f80adb659cd6ac697d41581c7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 7 Nov 2019 14:13:14 +0000 Subject: [PATCH 102/126] add hyper links. --- src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml | 12 +++- .../AboutAvaloniaDialog.xaml.cs | 55 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml index d7fcb22b95..ffc23cc5b0 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml @@ -73,6 +73,14 @@ - + + +