From f29aeffecd7e95d2d63669b648c7a8580c58ffaa Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 3 Oct 2019 18:01:04 +0200 Subject: [PATCH 01/61] 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 02/61] 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 03/61] 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 04/61] 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 7dc95d5644bd3b14d2760cb664599e24fc239344 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 23 Oct 2019 17:07:28 +0300 Subject: [PATCH 05/61] Adopted Conributor Covenant v1.4 --- CODE_OF_CONDUCT.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..dc57a73f48 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at steven@avaloniaui.net. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq From ef0464a7e76dfe2182782c03786e7258ecf34479 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 25 Oct 2019 11:10:37 +0300 Subject: [PATCH 06/61] 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 07/61] 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 08/61] 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 09/61] 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 10/61] 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 11/61] 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 12/61] 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 13/61] 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 14/61] 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 15/61] 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 16/61] 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 17/61] 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 18/61] 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 19/61] 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 20/61] 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 21/61] 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 22/61] 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 ab26c18a8f882b1665cbd62dfdbc5a877c5c534e Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sat, 26 Oct 2019 21:01:43 +0300 Subject: [PATCH 23/61] 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 9c5e688958c2f2590644d8e54f1951d92d8551c6 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 27 Oct 2019 22:13:49 +0100 Subject: [PATCH 24/61] 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 25/61] 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 58b79a7724f1d492758a60bc7723abbc65b3be3b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 18:47:18 +0100 Subject: [PATCH 26/61] 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 27/61] 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 28/61] 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 29/61] 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 30/61] 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 31/61] 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 32/61] 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 33/61] 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 34/61] 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 35/61] 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 b7a3747da66b5d0feae56dc10636c0f10b695ed2 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Wed, 30 Oct 2019 23:26:34 +0100 Subject: [PATCH 36/61] 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 d59d0904f43997a8cc4dcbb6064d141543045091 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 31 Oct 2019 11:20:33 +0200 Subject: [PATCH 37/61] 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 38/61] 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 39/61] 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 40/61] 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 41/61] 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 42/61] 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 43/61] 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 44/61] 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 45/61] 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 a612ee5a3eb1f5d80dfd540e5c64f5d998139784 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 1 Nov 2019 18:04:12 +0200 Subject: [PATCH 46/61] 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 47/61] 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 48/61] 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 f3bb98756f694f45f0fd6b47c6164495721e5134 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 3 Nov 2019 01:31:24 +0200 Subject: [PATCH 49/61] add tests for #3165 --- .../WrapPanelTests.cs | 78 +++++++++++++------ 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs index fd93df46b8..82f94b4f3c 100644 --- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs @@ -12,14 +12,14 @@ namespace Avalonia.Controls.UnitTests public void Lays_Out_Horizontally_On_Separate_Lines() { var target = new WrapPanel() - { - Width = 100, - Children = + { + Width = 100, + Children = { new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 }, } - }; + }; target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); @@ -33,14 +33,14 @@ namespace Avalonia.Controls.UnitTests public void Lays_Out_Horizontally_On_A_Single_Line() { var target = new WrapPanel() - { - Width = 200, - Children = + { + Width = 200, + Children = { new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 }, } - }; + }; target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); @@ -54,15 +54,15 @@ namespace Avalonia.Controls.UnitTests public void Lays_Out_Vertically_Children_On_A_Single_Line() { var target = new WrapPanel() - { - Orientation = Orientation.Vertical, - Height = 120, - Children = + { + Orientation = Orientation.Vertical, + Height = 120, + Children = { new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 }, } - }; + }; target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); @@ -76,15 +76,15 @@ namespace Avalonia.Controls.UnitTests public void Lays_Out_Vertically_On_Separate_Lines() { var target = new WrapPanel() - { - Orientation = Orientation.Vertical, - Height = 60, - Children = + { + Orientation = Orientation.Vertical, + Height = 60, + Children = { new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 }, } - }; + }; target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); @@ -98,17 +98,17 @@ namespace Avalonia.Controls.UnitTests public void Applies_ItemWidth_And_ItemHeight_Properties() { var target = new WrapPanel() - { - Orientation = Orientation.Horizontal, - Width = 50, - ItemWidth = 20, - ItemHeight = 15, - Children = + { + Orientation = Orientation.Horizontal, + Width = 50, + ItemWidth = 20, + ItemHeight = 15, + Children = { new Border(), new Border(), } - }; + }; target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); @@ -117,5 +117,33 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 0, 20, 15), target.Children[0].Bounds); Assert.Equal(new Rect(20, 0, 20, 15), target.Children[1].Bounds); } + + [Fact] + void ItemWidth_Trigger_InvalidateMeasure() + { + var target = new WrapPanel(); + + target.Measure(new Size(10, 10)); + + Assert.True(target.IsMeasureValid); + + target.ItemWidth = 1; + + Assert.False(target.IsMeasureValid); + } + + [Fact] + void ItemHeight_Trigger_InvalidateMeasure() + { + var target = new WrapPanel(); + + target.Measure(new Size(10, 10)); + + Assert.True(target.IsMeasureValid); + + target.ItemHeight = 1; + + Assert.False(target.IsMeasureValid); + } } } From 872cef0176713fdc31514ea2b03751eaaa2ee387 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sun, 3 Nov 2019 11:33:57 +0100 Subject: [PATCH 50/61] Introduce abstract font manager --- src/Avalonia.Visuals/Media/FontFamily.cs | 6 +- src/Avalonia.Visuals/Media/FontManager.cs | 106 +++++++++++++----- .../Media/Fonts/FamilyNameCollection.cs | 2 +- src/Avalonia.Visuals/Media/GlyphTypeface.cs | 2 +- src/Avalonia.Visuals/Media/Typeface.cs | 2 +- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 2 +- .../Media/GlyphTypefaceImpl.cs | 2 +- 7 files changed, 89 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index d263097e6a..b57b4a0ca8 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -8,11 +8,11 @@ using Avalonia.Media.Fonts; namespace Avalonia.Media { - public class FontFamily + public sealed class FontFamily { static FontFamily() { - Default = new FontFamily(FontManager.DefaultFontFamilyName); + Default = new FontFamily(FontManager.Default.DefaultFontFamilyName); } /// @@ -64,7 +64,7 @@ namespace Avalonia.Media /// Consider using the new instead. /// public static IEnumerable SystemFontFamilies => - FontManager.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); + FontManager.Default.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 95d91e7df1..be1bd269ed 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -7,50 +7,106 @@ using Avalonia.Platform; namespace Avalonia.Media { - public static class FontManager + /// + /// The font manager is used to query the system's installed fonts and is responsible for caching loaded fonts. + /// It is also responsible for the font fallback. + /// + public abstract class FontManager { - private static readonly IFontManagerImpl s_platformImpl = GetPlatformImpl(); + public static readonly FontManager Default = CreateDefault(); - /// - public static string DefaultFontFamilyName => s_platformImpl.DefaultFontFamilyName; + /// + /// Gets the system's default font family's name. + /// + public string DefaultFontFamilyName + { + get; + protected set; + } - /// - public static IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - s_platformImpl.GetInstalledFontFamilyNames(checkForUpdates); + /// + /// Get all installed fonts in the system. + /// If true the font collection is updated. + /// + public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); - /// - public static Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => - s_platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle); + /// + /// Get a cached typeface from specified parameters. + /// + /// The font family. + /// The font weight. + /// The font style. + /// + /// The cached typeface. + /// + public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); - /// - public static Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, + /// + /// Tries to match a specified character to a typeface that supports specified font properties. + /// Returns null if no fallback was found. + /// + /// 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 matched typeface. + /// + public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) => - s_platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture); + FontFamily fontFamily = null, CultureInfo culture = null); - private static IFontManagerImpl GetPlatformImpl() + public static FontManager CreateDefault() { var platformImpl = AvaloniaLocator.Current.GetService(); - return platformImpl ?? new EmptyFontManagerImpl(); + if (platformImpl != null) + { + return new PlatformFontManager(platformImpl); + } + + return new EmptyFontManager(); } - private class EmptyFontManagerImpl : IFontManagerImpl + private class PlatformFontManager : FontManager { - public string DefaultFontFamilyName => "Arial"; - - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => new[] { "Arial" }; + private readonly IFontManagerImpl _platformImpl; - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + public PlatformFontManager(IFontManagerImpl platformImpl) { - return new Typeface(fontFamily, fontWeight, fontStyle); + _platformImpl = platformImpl; + + DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName; } - public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) + public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + _platformImpl.GetInstalledFontFamilyNames(checkForUpdates); + + public override Typeface GetCachedTypeface(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 + { + public EmptyFontManager() { - return null; + DefaultFontFamilyName = "Empty"; } + + public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + new[] { DefaultFontFamilyName }; + + public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => new Typeface(fontFamily, fontWeight, fontStyle); + + public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, + FontStyle fontStyle = default, + FontFamily fontFamily = null, CultureInfo culture = null) => null; } } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index 0c161131dc..e777d93315 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 : IReadOnlyList + public sealed class FamilyNameCollection : IReadOnlyList { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs index cba7c8c795..3ba31f7e84 100644 --- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -7,7 +7,7 @@ using Avalonia.Platform; namespace Avalonia.Media { - public class GlyphTypeface : IDisposable + public sealed class GlyphTypeface : IDisposable { private static readonly IPlatformRenderInterface s_platformRenderInterface = AvaloniaLocator.Current.GetService(); diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 2d3c7e6ffa..a6d5c8a43c 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media /// /// Represents a typeface. /// - [DebuggerDisplay("Name = {FontFamily.Name}, Style = {Style}, Weight = {Weight}")] + [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")] public class Typeface : IEquatable { public static readonly Typeface Default = new Typeface(FontFamily.Default); diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index b9b5b07d7d..e46f766255 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -10,7 +10,7 @@ using SkiaSharp; namespace Avalonia.Skia { - internal class GlyphTypefaceImpl : IGlyphTypefaceImpl + public class GlyphTypefaceImpl : IGlyphTypefaceImpl { private bool _isDisposed; diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs index 66cf397110..32def01c39 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs @@ -9,7 +9,7 @@ using SharpDX.DirectWrite; namespace Avalonia.Direct2D1.Media { - internal class GlyphTypefaceImpl : IGlyphTypefaceImpl + public class GlyphTypefaceImpl : IGlyphTypefaceImpl { private bool _isDisposed; From e126874c80108e1e47c41cfafb1db9254dfcf256 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Nov 2019 11:25:32 +0100 Subject: [PATCH 51/61] Handle cross-root reparenting. When updating the scene for a window, moving a control to a different root should be the same as just removing the control. --- src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 558e96e132..1689f2e964 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -51,7 +51,7 @@ namespace Avalonia.Rendering.SceneGraph UpdateSize(scene); } - if (visual.VisualRoot != null) + if (visual.VisualRoot == scene.Root.Visual) { if (node?.Parent != null && visual.VisualParent != null && From dcf52fce844916f174cf89617f439c9f5935524e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 4 Nov 2019 14:35:58 +0100 Subject: [PATCH 52/61] Use a new macosx SDK --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 92e4afdca8..04e7262930 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -49,7 +49,7 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx10.14' + sdk: 'macosx10.15' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' xcodeVersion: 'default' # Options: 8, 9, default, specifyPath From bca8bc2fceb3915e74d3da316aab1cd614e6b476 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 4 Nov 2019 15:00:06 +0100 Subject: [PATCH 53/61] Update azure-pipelines.yml --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 04e7262930..4fe4d825bb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -49,10 +49,10 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx10.15' + sdk: 'macosx10.14' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' - xcodeVersion: 'default' # Options: 8, 9, default, specifyPath + xcodeVersion: '10.3' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - task: CmdLine@2 From 08fdce729eef0cff19afeb365c1fc840170088fb Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 4 Nov 2019 15:09:11 +0100 Subject: [PATCH 54/61] Update azure-pipelines.yml --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4fe4d825bb..603308ef9a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -52,7 +52,7 @@ jobs: sdk: 'macosx10.14' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' - xcodeVersion: '10.3' # Options: 8, 9, default, specifyPath + xcodeVersion: '10' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - task: CmdLine@2 From 5921d359c83a4149680265a9358172fe7e52b33d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Nov 2019 15:08:21 +0000 Subject: [PATCH 55/61] fix hit testing skia formatted text. --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index eb7b65cdce..718d10a694 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -99,9 +99,17 @@ namespace Avalonia.Skia public TextHitTestResult HitTestPoint(Point point) { float y = (float)point.Y; - var line = _skiaLines.Find(l => l.Top <= y && (l.Top + l.Height) > y); - if (!line.Equals(default(AvaloniaFormattedTextLine))) + AvaloniaFormattedTextLine line = default; + + int i = 0; + + while(_skiaLines[i].Top < y && i < _skiaLines.Count) + { + line = _skiaLines[i++]; + } + + if (!line.Equals(default)) { var rects = GetRects(); From 075aeecebf1694d4c35c27f75a164a9280e9f784 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Nov 2019 15:59:17 +0000 Subject: [PATCH 56/61] fix skia formatted text impl hit testing. --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 28 +++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 718d10a694..dfdbfef136 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -102,12 +102,17 @@ namespace Avalonia.Skia AvaloniaFormattedTextLine line = default; - int i = 0; - - while(_skiaLines[i].Top < y && i < _skiaLines.Count) + foreach(var currentLine in _skiaLines) { - line = _skiaLines[i++]; - } + if(currentLine.Top <= y) + { + line = currentLine; + } + else + { + break; + } + } if (!line.Equals(default)) { @@ -135,12 +140,15 @@ namespace Avalonia.Skia line.Length : (line.Length - 1); } - return new TextHitTestResult + if (y < line.Top + line.Height) { - IsInside = false, - TextPosition = line.Start + offset, - IsTrailing = Text.Length == (line.Start + offset + 1) - }; + return new TextHitTestResult + { + IsInside = false, + TextPosition = line.Start + offset, + IsTrailing = Text.Length == (line.Start + offset + 1) + }; + } } bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height); From eb1241eea36d45b000792f373952e9e58df6e321 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Nov 2019 16:26:45 +0000 Subject: [PATCH 57/61] fix skia formatted text unit tests. --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index dfdbfef136..a5e56b1d7a 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -102,14 +102,18 @@ namespace Avalonia.Skia AvaloniaFormattedTextLine line = default; + float nextTop = 0; + foreach(var currentLine in _skiaLines) { if(currentLine.Top <= y) { line = currentLine; + nextTop = currentLine.Top + currentLine.Height; } else { + nextTop = currentLine.Top; break; } } @@ -140,7 +144,7 @@ namespace Avalonia.Skia line.Length : (line.Length - 1); } - if (y < line.Top + line.Height) + if (y < nextTop) { return new TextHitTestResult { From 620be6f1050c014576f8079e6abbd29acc90772b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Nov 2019 09:32:22 +0000 Subject: [PATCH 58/61] add failing unit test for empty textbox hit test. --- tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index 353123ab2a..8eedb1dde7 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -157,6 +157,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media [InlineData(stringmiddle3lines, 500, 13, false, false, 8)] [InlineData(stringmiddle3lines, 30, 25, false, false, 9)] [InlineData(stringmiddle3lines, -1, 30, false, false, 10)] + [InlineData("", 0, 0, false, false, 0)] public void Should_HitTestPoint_Correctly(string input, double x, double y, bool isInside, bool isTrailing, int pos) From 69c24dc99df33a7eb8ee544ccba0a465acd9a632 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Nov 2019 09:33:09 +0000 Subject: [PATCH 59/61] fix bug in skia empty formatted text hit test. --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index a5e56b1d7a..07e3700fc2 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -118,7 +118,7 @@ namespace Avalonia.Skia } } - if (!line.Equals(default)) + if (!line.Equals(default(AvaloniaFormattedTextLine))) { var rects = GetRects(); From 86d21f27a7fec5cb595fa6a7b1432611f9f26ee8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Nov 2019 10:01:25 +0000 Subject: [PATCH 60/61] fix empty text hit test. direct2d --- src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index b73deb1f0a..ede66fb265 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -72,7 +72,7 @@ namespace Avalonia.Direct2D1.Media return new TextHitTestResult { - IsInside = isInside, + IsInside = result.Width == 0 ? false : (bool)isInside, TextPosition = result.TextPosition, IsTrailing = isTrailingHit, }; From 5b3e2320a8eff35156467ce8b4025ca2df7827bb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Nov 2019 10:15:36 +0000 Subject: [PATCH 61/61] remove unit tests, skia and direct2d behave differently and making them match is beyond the scope of this bug fix. --- src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs | 2 +- tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index ede66fb265..b73deb1f0a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -72,7 +72,7 @@ namespace Avalonia.Direct2D1.Media return new TextHitTestResult { - IsInside = result.Width == 0 ? false : (bool)isInside, + IsInside = isInside, TextPosition = result.TextPosition, IsTrailing = isTrailingHit, }; diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index 8eedb1dde7..353123ab2a 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -157,7 +157,6 @@ namespace Avalonia.Direct2D1.RenderTests.Media [InlineData(stringmiddle3lines, 500, 13, false, false, 8)] [InlineData(stringmiddle3lines, 30, 25, false, false, 9)] [InlineData(stringmiddle3lines, -1, 30, false, false, 10)] - [InlineData("", 0, 0, false, false, 0)] public void Should_HitTestPoint_Correctly(string input, double x, double y, bool isInside, bool isTrailing, int pos)