From f564fd4ed9bf302f13c88d1a15ae7e2ff3f3b9e4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 13 Feb 2020 12:40:57 +0100 Subject: [PATCH 01/13] Update readme.md --- readme.md | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/readme.md b/readme.md index 42b1e52205..40471b3b28 100644 --- a/readme.md +++ b/readme.md @@ -8,25 +8,21 @@ ## About -**Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS and with experimental support for Android and iOS. +**Avalonia** is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS. -**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and [breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). +**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. -| Control catalog | Desktop platforms | Mobile platforms | -|---|---|---| -| | | | +To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). -[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is curated list of awesome Avalonia UI tools, libraries, projects and resources. +You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. -## Getting Started - -Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (screenshot). Now you can write code and markup that will work on multiple platforms! +[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia! -For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). +## Getting Started -If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen. +The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project). -Avalonia is delivered via NuGet package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/) +Avalonia is delivered via NuGet package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/ Use these commands in the Package Manager console to install Avalonia manually: ``` @@ -34,18 +30,17 @@ Install-Package Avalonia Install-Package Avalonia.Desktop ``` -## Bleeding Edge Builds +## JetBrains Rider -or use nightly build feeds as described here: -https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed +If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen. -## Documentation +## Bleeding Edge Builds -You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). +We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) which tracks the current state of master. Although these packages are less stable than the release on NuGet.org, you'll get all the latest features and bugfixes right away and many of our users actually prefer this feed! -There's also a high-level [architecture document](http://avaloniaui.net/architecture/project-structure) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/. +## Documentation -Contributions for our docs are always welcome! +Documentation can be found on our website at http://avaloniaui.net/docs/. We also have a [tutorial](http://avaloniaui.net/docs/tutorial/) over there for newcomers. ## Building and Using @@ -60,14 +55,12 @@ Please read the [contribution guidelines](http://avaloniaui.net/contributing/con This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)]. - ### Backers Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)] - ### Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/Avalonia#sponsor)] From 6f96d464afc4fcb8116d845d7f3068d7e27b2ce0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Feb 2020 14:27:33 +0100 Subject: [PATCH 02/13] Pass ResourceChanged messages to child styles. Fixes #3590. --- src/Avalonia.Styling/StyledElement.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 84df9d4b94..a3a5d7f53a 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -213,6 +213,7 @@ namespace Avalonia { _styles = new Styles(this); _styles.ResourcesChanged += ThisResourcesChanged; + NotifyResourcesChanged(new ResourcesChangedEventArgs()); } return _styles; From 7fb62b20fae4049d585cafd1a34d11530168f365 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Feb 2020 16:21:34 +0100 Subject: [PATCH 03/13] Don't allow StyledElement.Styles to be set. It's not settable in Application and I can't see a reason it should be in `StyledElement`. Also add a `Styles` ctor that takes a parent. --- src/Avalonia.Styling/StyledElement.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index a3a5d7f53a..6b020dd1c0 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -209,11 +209,10 @@ namespace Avalonia { get { - if (_styles is null) + if (_styles == null) { _styles = new Styles(this); _styles.ResourcesChanged += ThisResourcesChanged; - NotifyResourcesChanged(new ResourcesChangedEventArgs()); } return _styles; From 6b902254bcc0c251d0c2255da2c1520ec04b63a5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Feb 2020 17:52:06 +0100 Subject: [PATCH 04/13] Refactored styling. - Don't use Rx in the styling system. Instead introduces `IStyleActivator` which is like an `IObservable`-lite in order to cut down on allocations. - #nullable enable on touched files --- src/Avalonia.Styling/StyledElement.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 6b020dd1c0..0bd01d2634 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -207,9 +207,9 @@ namespace Avalonia /// public Styles Styles { - get + get { - if (_styles == null) + if (_styles is null) { _styles = new Styles(this); _styles.ResourcesChanged += ThisResourcesChanged; From 3dcaa174bfbf1eeac9bc393dba33a68cb0ddeb70 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 3 Mar 2020 09:27:06 +0100 Subject: [PATCH 05/13] Initial tests for add/remove styles. --- .../Avalonia.Styling.UnitTests/StyleTests.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/Avalonia.Styling.UnitTests/StyleTests.cs b/tests/Avalonia.Styling.UnitTests/StyleTests.cs index 4c9b18c886..ef53e65aa6 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyleTests.cs @@ -244,6 +244,72 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(new Thickness(0), border.BorderThickness); } + [Fact] + public void Style_Should_Be_Detached_From_Control_When_Removed() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var border = new Border(); + var root = new TestRoot + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(4)), + } + } + }, + Child = border, + }; + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(4), border.BorderThickness); + + root.Styles.RemoveAt(0); + Assert.Equal(new Thickness(0), border.BorderThickness); + } + } + + [Fact] + public void Style_Should_Be_Atttached_To_Control_When_Added() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var border = new Border(); + var root = new TestRoot + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(4)), + } + } + }, + Child = border, + }; + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(4), border.BorderThickness); + + root.Styles.Add(new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(6)), + } + }); + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(6), border.BorderThickness); + } + } + private class Class1 : Control { public static readonly StyledProperty FooProperty = From e9256a9e40027c721d5a6ff80bb2982c0e27fb0e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 3 Mar 2020 13:18:54 +0100 Subject: [PATCH 06/13] Initial implementation of add/remove styles. Only currently works on top-level `Styles` collections, not working in `Application.Styles`. --- src/Avalonia.Controls/Application.cs | 10 +- src/Avalonia.Layout/Layoutable.cs | 7 + src/Avalonia.Styling/StyledElement.cs | 102 ++++++++++++-- src/Avalonia.Styling/Styling/IStyleHost.cs | 16 +++ src/Avalonia.Styling/Styling/IStyleable.cs | 11 ++ src/Avalonia.Styling/Styling/Styles.cs | 130 ++++++++++++------ .../AvaloniaPropertyConverterTest.cs | 11 ++ 7 files changed, 236 insertions(+), 51 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index d7b1aec0b7..c5d624da64 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Reactive.Concurrency; using System.Threading; using Avalonia.Animation; @@ -214,6 +215,14 @@ namespace Avalonia Styles.TryGetResource(key, out value); } + void IStyleHost.StylesAdded(IReadOnlyList styles) + { + } + + void IStyleHost.StylesRemoved(IReadOnlyList styles) + { + } + /// /// Register's the services needed by Avalonia. /// @@ -286,6 +295,5 @@ namespace Avalonia get => _name; set => SetAndRaise(NameProperty, ref _name, value); } - } } diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 9d0a5c57ee..5fbcb45d4b 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -507,6 +507,7 @@ namespace Avalonia.Layout { var margin = Margin; + ApplyStyling(); ApplyTemplate(); var constrained = LayoutHelper.ApplyLayoutConstraints( @@ -692,6 +693,12 @@ namespace Avalonia.Layout return finalSize; } + protected sealed override void InvalidateStyles() + { + base.InvalidateStyles(); + InvalidateMeasure(); + } + /// protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) { diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 0bd01d2634..cee30efcf0 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -352,21 +352,34 @@ namespace Avalonia if (--_initCount == 0 && _logicalRoot != null) { - InitializeStylesIfNeeded(); - + ApplyStyling(); InitializeIfNeeded(); } } - private void InitializeStylesIfNeeded(bool force = false) + /// + /// Applies styling to the control if the control is initialized and styling is not + /// already applied. + /// + /// + /// A value indicating whether styling is now applied to the control. + /// + protected bool ApplyStyling() { - if (_initCount == 0 && (!_styled || force)) + if (_initCount == 0 && !_styled) { - ApplyStyling(); + AvaloniaLocator.Current.GetService()?.ApplyStyles(this); _styled = true; } + + return _styled; } + /// + /// Detaches all styles from the element and queues a restyle. + /// + protected virtual void InvalidateStyles() => DetachStyles(); + protected void InitializeIfNeeded() { if (_initCount == 0 && !IsInitialized) @@ -480,6 +493,20 @@ namespace Avalonia void IStyleable.DetachStyles() => DetachStyles(); + void IStyleable.DetachStyles(IReadOnlyList styles) => DetachStyles(styles); + + void IStyleable.InvalidateStyles() => InvalidateStyles(); + + void IStyleHost.StylesAdded(IReadOnlyList styles) + { + InvalidateStylesOnThisAndDescendents(); + } + + void IStyleHost.StylesRemoved(IReadOnlyList styles) + { + DetachStylesFromThisAndDescendents(styles); + } + protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -604,11 +631,6 @@ namespace Avalonia return null; } - private void ApplyStyling() - { - AvaloniaLocator.Current.GetService()?.ApplyStyles(this); - } - private static void ValidateLogicalChild(ILogical c) { if (c == null) @@ -635,7 +657,7 @@ namespace Avalonia { _logicalRoot = e.Root; - InitializeStylesIfNeeded(true); + ApplyStyling(); OnAttachedToLogicalTree(e); AttachedToLogicalTree?.Invoke(this, e); @@ -713,6 +735,64 @@ namespace Avalonia _appliedStyles.Clear(); } + + _styled = false; + } + + private void DetachStyles(IReadOnlyList styles) + { + styles = styles ?? throw new ArgumentNullException(nameof(styles)); + + if (_appliedStyles is null) + { + return; + } + + var count = styles.Count; + + for (var i = 0; i < count; ++i) + { + for (var j = _appliedStyles.Count - 1; j >= 0; --j) + { + var applied = _appliedStyles[j]; + + if (applied.Source == styles[i]) + { + applied.Dispose(); + _appliedStyles.RemoveAt(j); + } + } + } + } + + private void InvalidateStylesOnThisAndDescendents() + { + InvalidateStyles(); + + if (_logicalChildren is object) + { + var childCount = _logicalChildren.Count; + + for (var i = 0; i < childCount; ++i) + { + (_logicalChildren[i] as StyledElement)?.InvalidateStylesOnThisAndDescendents(); + } + } + } + + private void DetachStylesFromThisAndDescendents(IReadOnlyList styles) + { + DetachStyles(styles); + + if (_logicalChildren is object) + { + var childCount = _logicalChildren.Count; + + for (var i = 0; i < childCount; ++i) + { + (_logicalChildren[i] as StyledElement)?.DetachStylesFromThisAndDescendents(styles); + } + } } private void ClearLogicalParent(IEnumerable children) diff --git a/src/Avalonia.Styling/Styling/IStyleHost.cs b/src/Avalonia.Styling/Styling/IStyleHost.cs index fa722aeb41..73726f3056 100644 --- a/src/Avalonia.Styling/Styling/IStyleHost.cs +++ b/src/Avalonia.Styling/Styling/IStyleHost.cs @@ -2,6 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections.Generic; + +#nullable enable + namespace Avalonia.Styling { /// @@ -27,5 +31,17 @@ namespace Avalonia.Styling /// Gets the parent style host element. /// IStyleHost StylingParent { get; } + + /// + /// Called when styles are added to . + /// + /// The added styles. + void StylesAdded(IReadOnlyList styles); + + /// + /// Called when styles are removed from . + /// + /// The removed styles. + void StylesRemoved(IReadOnlyList styles); } } diff --git a/src/Avalonia.Styling/Styling/IStyleable.cs b/src/Avalonia.Styling/Styling/IStyleable.cs index b01c779bcc..34687b070c 100644 --- a/src/Avalonia.Styling/Styling/IStyleable.cs +++ b/src/Avalonia.Styling/Styling/IStyleable.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using Avalonia.Collections; #nullable enable @@ -38,5 +39,15 @@ namespace Avalonia.Styling /// Detaches all styles applied to the element. /// void DetachStyles(); + + /// + /// Detaches a collection of styles, if applied to the element. + /// + void DetachStyles(IReadOnlyList styles); + + /// + /// Detaches all styles from the element and queues a restyle. + /// + void InvalidateStyles(); } } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 05cbba5ad2..edd729679d 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -27,40 +27,7 @@ namespace Avalonia.Styling public Styles() { _styles.ResetBehavior = ResetBehavior.Remove; - _styles.ForEachItem( - x => - { - if (x.ResourceParent == null && x is ISetResourceParent setParent) - { - setParent.SetParent(this); - setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); - } - - if (x.HasResources) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } - - x.ResourcesChanged += NotifyResourcesChanged; - _cache = null; - }, - x => - { - if (x.ResourceParent == this && x is ISetResourceParent setParent) - { - setParent.SetParent(null); - setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); - } - - if (x.HasResources) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } - - x.ResourcesChanged -= NotifyResourcesChanged; - _cache = null; - }, - () => { }); + _styles.CollectionChanged += OnCollectionChanged; } public Styles(IResourceNode parent) @@ -69,11 +36,7 @@ namespace Avalonia.Styling _parent = parent; } - public event NotifyCollectionChangedEventHandler CollectionChanged - { - add => _styles.CollectionChanged += value; - remove => _styles.CollectionChanged -= value; - } + public event NotifyCollectionChangedEventHandler? CollectionChanged; /// public event EventHandler? ResourcesChanged; @@ -257,6 +220,95 @@ namespace Avalonia.Styling NotifyResourcesChanged(e); } + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + static IReadOnlyList ToReadOnlyList(IList list) + { + if (list is IReadOnlyList) + { + return (IReadOnlyList)list; + } + else + { + var result = new T[list.Count]; + list.CopyTo(result, 0); + return result; + } + } + + void Add(IList items) + { + for (var i = 0; i < items.Count; ++i) + { + var style = (IStyle)items[i]; + + if (style.ResourceParent == null && style is ISetResourceParent setParent) + { + setParent.SetParent(this); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); + } + + if (style.HasResources) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + + style.ResourcesChanged += NotifyResourcesChanged; + _cache = null; + } + + if (_parent is IStyleHost host) + { + host.StylesAdded(ToReadOnlyList(items)); + } + } + + void Remove(IList items) + { + for (var i = 0; i < items.Count; ++i) + { + var style = (IStyle)items[i]; + + if (style.ResourceParent == this && style is ISetResourceParent setParent) + { + setParent.SetParent(null); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); + } + + if (style.HasResources) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + + style.ResourcesChanged -= NotifyResourcesChanged; + _cache = null; + } + + if (_parent is IStyleHost host) + { + host.StylesRemoved(ToReadOnlyList(items)); + } + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + Remove(e.OldItems); + break; + case NotifyCollectionChangedAction.Replace: + Remove(e.OldItems); + Add(e.NewItems); + break; + case NotifyCollectionChangedAction.Reset: + throw new InvalidOperationException("Reset should not be called on Styles."); + } + + CollectionChanged?.Invoke(this, e); + } + private void NotifyResourcesChanged(object sender, ResourcesChangedEventArgs e) { NotifyResourcesChanged(e); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index ec9a6ba77f..d4953bf47e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -9,6 +9,7 @@ using Avalonia.Styling; using Xunit; using System.ComponentModel; using Avalonia.Markup.Xaml.XamlIl.Runtime; +using System.Collections.Generic; namespace Avalonia.Markup.Xaml.UnitTests.Converters { @@ -144,6 +145,16 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters throw new NotImplementedException(); } + public void DetachStyles(IReadOnlyList styles) + { + throw new NotImplementedException(); + } + + public void InvalidateStyles() + { + throw new NotImplementedException(); + } + public void StyleApplied(IStyleInstance instance) { throw new NotImplementedException(); From 6db44298bb8f7670df0cee295c0164d470ca0833 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 5 Mar 2020 14:30:05 +0100 Subject: [PATCH 07/13] Handle adding/removing nested styles. --- src/Avalonia.Styling/StyledElement.cs | 40 +++++- src/Avalonia.Styling/Styling/IStyle.cs | 8 +- src/Avalonia.Styling/Styling/IStyleHost.cs | 4 +- src/Avalonia.Styling/Styling/Style.cs | 2 + src/Avalonia.Styling/Styling/Styles.cs | 29 +++-- .../Styling/StyleInclude.cs | 23 ++-- .../Avalonia.Styling.UnitTests/StyleTests.cs | 117 +++++++++++++++++- 7 files changed, 200 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index cee30efcf0..7b5cf4b7fa 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -504,7 +504,8 @@ namespace Avalonia void IStyleHost.StylesRemoved(IReadOnlyList styles) { - DetachStylesFromThisAndDescendents(styles); + var allStyles = RecurseStyles(styles); + DetachStylesFromThisAndDescendents(allStyles); } protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -830,5 +831,42 @@ namespace Avalonia { NotifyResourcesChanged(e); } + + private static IReadOnlyList RecurseStyles(IReadOnlyList styles) + { + var count = styles.Count; + List? result = null; + + for (var i = 0; i < count; ++i) + { + var style = styles[i]; + + if (style.Children.Count > 0) + { + if (result is null) + { + result = new List(styles); + } + + RecurseStyles(style.Children, result); + } + } + + return result ?? styles; + } + + private static void RecurseStyles(IReadOnlyList styles, List result) + { + var count = styles.Count; + + result.Capacity += count; + + for (var i = 0; i < count; ++i) + { + var style = styles[i]; + result.Add(style); + RecurseStyles(style.Children, result); + } + } } } diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs index 8151aacf54..18866b0060 100644 --- a/src/Avalonia.Styling/Styling/IStyle.cs +++ b/src/Avalonia.Styling/Styling/IStyle.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.Generic; using Avalonia.Controls; #nullable enable @@ -13,7 +14,12 @@ namespace Avalonia.Styling public interface IStyle : IResourceNode { /// - /// Attaches the style to a control if the style's selector matches. + /// Gets a collection of child styles. + /// + IReadOnlyList Children { get; } + + /// + /// Attaches the style and any child styles to a control if the style's selector matches. /// /// The control to attach to. /// The element that hosts the style. diff --git a/src/Avalonia.Styling/Styling/IStyleHost.cs b/src/Avalonia.Styling/Styling/IStyleHost.cs index 73726f3056..182662b32e 100644 --- a/src/Avalonia.Styling/Styling/IStyleHost.cs +++ b/src/Avalonia.Styling/Styling/IStyleHost.cs @@ -33,13 +33,13 @@ namespace Avalonia.Styling IStyleHost StylingParent { get; } /// - /// Called when styles are added to . + /// Called when styles are added to or a nested styles collection. /// /// The added styles. void StylesAdded(IReadOnlyList styles); /// - /// Called when styles are removed from . + /// Called when styles are removed from or a nested styles collection. /// /// The removed styles. void StylesRemoved(IReadOnlyList styles); diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 22adf573e8..2e0f564522 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -90,6 +90,8 @@ namespace Avalonia.Styling /// bool IResourceProvider.HasResources => _resources?.Count > 0; + IReadOnlyList IStyle.Children => Array.Empty(); + /// public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index edd729679d..c9278339b5 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -84,6 +84,8 @@ namespace Avalonia.Styling /// IStyle IReadOnlyList.this[int index] => _styles[index]; + IReadOnlyList IStyle.Children => this; + /// public IStyle this[int index] { @@ -257,10 +259,7 @@ namespace Avalonia.Styling _cache = null; } - if (_parent is IStyleHost host) - { - host.StylesAdded(ToReadOnlyList(items)); - } + GetHost()?.StylesAdded(ToReadOnlyList(items)); } void Remove(IList items) @@ -284,10 +283,7 @@ namespace Avalonia.Styling _cache = null; } - if (_parent is IStyleHost host) - { - host.StylesRemoved(ToReadOnlyList(items)); - } + GetHost()?.StylesRemoved(ToReadOnlyList(items)); } switch (e.Action) @@ -309,6 +305,23 @@ namespace Avalonia.Styling CollectionChanged?.Invoke(this, e); } + private IStyleHost? GetHost() + { + var node = _parent; + + while (node != null) + { + if (node is IStyleHost host) + { + return host; + } + + node = node.ResourceParent; + } + + return null; + } + private void NotifyResourcesChanged(object sender, ResourcesChangedEventArgs e) { NotifyResourcesChanged(e); diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index e5e28b344f..46fc7c36da 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -6,6 +6,8 @@ using System; using Avalonia.Controls; using System.Collections.Generic; +#nullable enable + namespace Avalonia.Markup.Xaml.Styling { /// @@ -14,8 +16,8 @@ namespace Avalonia.Markup.Xaml.Styling public class StyleInclude : IStyle, ISetResourceParent { private Uri _baseUri; - private IStyle _loaded; - private IResourceNode _parent; + private IStyle[]? _loaded; + private IResourceNode? _parent; /// /// Initializes a new instance of the class. @@ -41,7 +43,7 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Gets or sets the source URL. /// - public Uri Source { get; set; } + public Uri? Source { get; set; } /// /// Gets the loaded style. @@ -53,11 +55,12 @@ namespace Avalonia.Markup.Xaml.Styling if (_loaded == null) { var loader = new AvaloniaXamlLoader(); - _loaded = (IStyle)loader.Load(Source, _baseUri); - (_loaded as ISetResourceParent)?.SetParent(this); + var loaded = (IStyle)loader.Load(Source, _baseUri); + (loaded as ISetResourceParent)?.SetParent(this); + _loaded = new[] { loaded }; } - return _loaded; + return _loaded?[0]!; } } @@ -65,13 +68,15 @@ namespace Avalonia.Markup.Xaml.Styling bool IResourceProvider.HasResources => Loaded.HasResources; /// - IResourceNode IResourceNode.ResourceParent => _parent; + IResourceNode? IResourceNode.ResourceParent => _parent; + + IReadOnlyList IStyle.Children => _loaded ?? Array.Empty(); /// - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost host) => Loaded.TryAttach(target, host); + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host); /// - public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value); + public bool TryGetResource(object key, out object? value) => Loaded.TryGetResource(key, out value); /// void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) diff --git a/tests/Avalonia.Styling.UnitTests/StyleTests.cs b/tests/Avalonia.Styling.UnitTests/StyleTests.cs index ef53e65aa6..619f72aab0 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyleTests.cs @@ -245,7 +245,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Style_Should_Be_Detached_From_Control_When_Removed() + public void Removing_Style_Should_Detach_From_Control() { using (UnitTestApplication.Start(TestServices.RealStyler)) { @@ -274,7 +274,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Style_Should_Be_Atttached_To_Control_When_Added() + public void Adding_Style_Should_Attach_To_Control() { using (UnitTestApplication.Start(TestServices.RealStyler)) { @@ -310,6 +310,119 @@ namespace Avalonia.Styling.UnitTests } } + [Fact] + public void Removing_Style_With_Nested_Style_Should_Detach_From_Control() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var border = new Border(); + var root = new TestRoot + { + Styles = + { + new Styles + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(4)), + } + } + } + }, + Child = border, + }; + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(4), border.BorderThickness); + + root.Styles.RemoveAt(0); + Assert.Equal(new Thickness(0), border.BorderThickness); + } + } + + [Fact] + public void Adding_Nested_Style_Should_Attach_To_Control() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var border = new Border(); + var root = new TestRoot + { + Styles = + { + new Styles + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(4)), + } + } + } + }, + Child = border, + }; + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(4), border.BorderThickness); + + ((Styles)root.Styles[0]).Add(new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(6)), + } + }); + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(6), border.BorderThickness); + } + } + + [Fact] + public void Removing_Nested_Style_Should_Detach_From_Control() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var border = new Border(); + var root = new TestRoot + { + Styles = + { + new Styles + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(4)), + } + }, + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(6)), + } + }, + } + }, + Child = border, + }; + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(6), border.BorderThickness); + + ((Styles)root.Styles[0]).RemoveAt(1); + + root.Measure(Size.Infinity); + Assert.Equal(new Thickness(4), border.BorderThickness); + } + } + private class Class1 : Control { public static readonly StyledProperty FooProperty = From cfae69dfdc210cc6851e56f90801df22a9b9b298 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Feb 2020 14:27:33 +0100 Subject: [PATCH 08/13] Pass ResourceChanged messages to child styles. Fixes #3590. --- src/Avalonia.Styling/StyledElement.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 7b5cf4b7fa..385ce597b9 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -213,6 +213,7 @@ namespace Avalonia { _styles = new Styles(this); _styles.ResourcesChanged += ThisResourcesChanged; + NotifyResourcesChanged(new ResourcesChangedEventArgs()); } return _styles; From 119a975610b3bdf455a0febda5f907823592485d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 5 Mar 2020 18:11:33 +0100 Subject: [PATCH 09/13] Handle adding/removing application styles. --- src/Avalonia.Controls/Application.cs | 16 ++++++++ src/Avalonia.Controls/TopLevel.cs | 20 ++++++++-- src/Avalonia.Styling/Styling/IGlobalStyles.cs | 14 +++++++ .../TopLevelTests.cs | 39 +++++++++++++++++++ 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index c5d624da64..f8838e8cfb 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -46,6 +46,8 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; private bool _notifyingResourcesChanged; + private Action> _stylesAdded; + private Action> _stylesRemoved; /// /// Defines the property. @@ -202,6 +204,18 @@ namespace Avalonia /// public IApplicationLifetime ApplicationLifetime { get; set; } + event Action> IGlobalStyles.GlobalStylesAdded + { + add => _stylesAdded += value; + remove => _stylesAdded -= value; + } + + event Action> IGlobalStyles.GlobalStylesRemoved + { + add => _stylesRemoved += value; + remove => _stylesRemoved -= value; + } + /// /// Initializes the application by loading XAML etc. /// @@ -217,10 +231,12 @@ namespace Avalonia void IStyleHost.StylesAdded(IReadOnlyList styles) { + _stylesAdded?.Invoke(styles); } void IStyleHost.StylesRemoved(IReadOnlyList styles) { + _stylesRemoved?.Invoke(styles); } /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index cabadac7d6..f477379777 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -50,6 +50,7 @@ namespace Avalonia.Controls private readonly IAccessKeyHandler _accessKeyHandler; private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; private readonly IPlatformRenderInterface _renderInterface; + private readonly IGlobalStyles _globalStyles; private Size _clientSize; private ILayoutManager _layoutManager; @@ -94,6 +95,7 @@ namespace Avalonia.Controls _inputManager = TryGetService(dependencyResolver); _keyboardNavigationHandler = TryGetService(dependencyResolver); _renderInterface = TryGetService(dependencyResolver); + _globalStyles = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); @@ -112,6 +114,13 @@ namespace Avalonia.Controls _keyboardNavigationHandler?.SetOwner(this); _accessKeyHandler?.SetOwner(this); + + if (_globalStyles is object) + { + _globalStyles.GlobalStylesAdded += ((IStyleHost)this).StylesAdded; + _globalStyles.GlobalStylesRemoved += ((IStyleHost)this).StylesRemoved; + } + styler?.ApplyStyles(this); ClientSize = impl.ClientSize; @@ -215,10 +224,7 @@ namespace Avalonia.Controls /// double IRenderRoot.RenderScaling => PlatformImpl?.Scaling ?? 1; - IStyleHost IStyleHost.StylingParent - { - get { return AvaloniaLocator.Current.GetService(); } - } + IStyleHost IStyleHost.StylingParent => _globalStyles; IRenderTarget IRenderRoot.CreateRenderTarget() => CreateRenderTarget(); @@ -267,6 +273,12 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { + if (_globalStyles is object) + { + _globalStyles.GlobalStylesAdded -= ((IStyleHost)this).StylesAdded; + _globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved; + } + var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); diff --git a/src/Avalonia.Styling/Styling/IGlobalStyles.cs b/src/Avalonia.Styling/Styling/IGlobalStyles.cs index 51393ef0b3..6152755a05 100644 --- a/src/Avalonia.Styling/Styling/IGlobalStyles.cs +++ b/src/Avalonia.Styling/Styling/IGlobalStyles.cs @@ -1,6 +1,11 @@ // 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; + +#nullable enable + namespace Avalonia.Styling { /// @@ -8,5 +13,14 @@ namespace Avalonia.Styling /// public interface IGlobalStyles : IStyleHost { + /// + /// Raised when styles are added to or a nested styles collection. + /// + public event Action> GlobalStylesAdded; + + /// + /// Raised when styles are removed from or a nested styles collection. + /// + public event Action> GlobalStylesRemoved; } } diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 91592b1ff7..26bf604187 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -9,6 +9,7 @@ using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Platform; +using Avalonia.Styling; using Avalonia.UnitTests; using Moq; using Xunit; @@ -269,6 +270,44 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Reacts_To_Changes_In_Global_Styles() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + impl.SetupGet(x => x.Scaling).Returns(1); + + var child = new Border { Classes = { "foo" } }; + var target = new TestTopLevel(impl.Object) + { + Template = CreateTemplate(), + Content = child, + }; + + target.LayoutManager.ExecuteInitialLayoutPass(target); + + Assert.Equal(new Thickness(0), child.BorderThickness); + + var style = new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Border.BorderThicknessProperty, new Thickness(2)) + } + }; + + Application.Current.Styles.Add(style); + target.LayoutManager.ExecuteInitialLayoutPass(target); + + Assert.Equal(new Thickness(2), child.BorderThickness); + + Application.Current.Styles.Remove(style); + + Assert.Equal(new Thickness(0), child.BorderThickness); + } + } + private FuncControlTemplate CreateTemplate() { return new FuncControlTemplate((x, scope) => From bd571c3459eb179271f28d929312ac8467523a37 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 18 Mar 2020 12:42:57 +0100 Subject: [PATCH 10/13] Remove manual capacity management. --- src/Avalonia.Styling/StyledElement.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 385ce597b9..241ca1d0ec 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -207,7 +207,7 @@ namespace Avalonia /// public Styles Styles { - get + get { if (_styles is null) { @@ -860,8 +860,6 @@ namespace Avalonia { var count = styles.Count; - result.Capacity += count; - for (var i = 0; i < count; ++i) { var style = styles[i]; From 30458c81c505f305f1520ae759d0b0a65a6f632b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 19 Mar 2020 13:10:37 -0300 Subject: [PATCH 11/13] Fix OSX binding to WindowState. --- native/Avalonia.Native/src/OSX/window.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 2c03407732..2c1208a38d 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -426,6 +426,7 @@ private: ComPtr WindowEvents; WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { + _lastWindowState = Normal; WindowEvents = events; [Window setCanBecomeKeyAndMain]; [Window disableCursorRects]; @@ -439,7 +440,7 @@ private: [[Window parentWindow] removeChildWindow:Window]; WindowBaseImpl::Show(); - return SetWindowState(Normal); + return SetWindowState(_lastWindowState); } } From f7aa466803deed6f030ff86fd1bb265d8d904a9b Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 20 Mar 2020 21:58:46 +0100 Subject: [PATCH 12/13] Include last line when constraint is surpassed --- .../Media/TextFormatting/TextLayout.cs | 27 ++++----- .../TextLayoutTests.cs | 58 ++++++++++++++----- 2 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 3f0cf7c680..4b5568e71a 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -233,16 +233,16 @@ namespace Avalonia.Media.TextFormatting var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties); + UpdateBounds(textLine, ref left, ref right, ref bottom); + + textLines.Add(textLine); + if (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight) { currentPosition = _text.Length; break; } - UpdateBounds(textLine, ref left, ref right, ref bottom); - - textLines.Add(textLine); - if (_paragraphProperties.TextTrimming != TextTrimming.None) { currentPosition += remainingLength; @@ -254,22 +254,15 @@ namespace Avalonia.Media.TextFormatting currentPosition += textLine.Text.Length; } + } - if (lineBreaker.Current.Required && currentPosition == _text.Length) - { - var emptyTextLine = CreateEmptyTextLine(currentPosition); - - if (!double.IsPositiveInfinity(MaxHeight) && bottom + emptyTextLine.LineMetrics.Size.Height > MaxHeight) - { - break; - } - - UpdateBounds(emptyTextLine, ref left, ref right, ref bottom); + if (lineBreaker.Current.Required && currentPosition == _text.Length) + { + var emptyTextLine = CreateEmptyTextLine(currentPosition); - textLines.Add(emptyTextLine); + UpdateBounds(emptyTextLine, ref left, ref right, ref bottom); - break; - } + textLines.Add(emptyTextLine); } Bounds = new Rect(left, 0, right, bottom); diff --git a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs index 702e2118f5..15ad51ec47 100644 --- a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs @@ -29,10 +29,10 @@ namespace Avalonia.Skia.UnitTests var layout = new TextLayout( s_multiLineText, - Typeface.Default, + Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides : spans); + textStyleOverrides: spans); var textLine = layout.TextLines[0]; @@ -72,16 +72,16 @@ namespace Avalonia.Skia.UnitTests 12.0f, Brushes.Black.ToImmutable(), textWrapping: TextWrapping.Wrap, - maxWidth : 25); + maxWidth: 25); var actual = new TextLayout( s_multiLineText, Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textWrapping : TextWrapping.Wrap, - maxWidth : 25, - textStyleOverrides : spans); + textWrapping: TextWrapping.Wrap, + maxWidth: 25, + textStyleOverrides: spans); Assert.Equal(expected.TextLines.Count, actual.TextLines.Count); @@ -115,7 +115,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides : spans); + textStyleOverrides: spans); var textLine = layout.TextLines[0]; @@ -153,7 +153,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides : spans); + textStyleOverrides: spans); var textLine = layout.TextLines[0]; @@ -190,7 +190,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides : spans); + textStyleOverrides: spans); var textLine = layout.TextLines[0]; @@ -301,8 +301,8 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textWrapping : TextWrapping.Wrap, - maxWidth : 180, + textWrapping: TextWrapping.Wrap, + maxWidth: 180, textStyleOverrides: spans); Assert.Equal( @@ -332,8 +332,8 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - maxWidth : 200, - maxHeight : 125, + maxWidth: 200, + maxHeight: 125, textStyleOverrides: spans); Assert.Equal(foreground, layout.TextLines[0].TextRuns[1].Style.Foreground); @@ -430,7 +430,7 @@ namespace Avalonia.Skia.UnitTests Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]); - if(expectedLength == 7) + if (expectedLength == 7) { Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]); } @@ -480,12 +480,38 @@ namespace Avalonia.Skia.UnitTests } } + [InlineData("0123456789\r0123456789", 2)] + [InlineData("0123456789", 1)] + [Theory] + public void Should_Include_Last_Line_When_Constraint_Is_Surpassed(string text, int numberOfLines) + { + using (Start()) + { + var glyphTypeface = Typeface.Default.GlyphTypeface; + + var emHeight = glyphTypeface.DesignEmHeight; + + var lineHeight = (glyphTypeface.Descent - glyphTypeface.Ascent) * (12.0 / emHeight); + + var layout = new TextLayout( + text, + Typeface.Default, + 12, + Brushes.Black.ToImmutable(), + maxHeight: lineHeight * numberOfLines - lineHeight * 0.5); + + Assert.Equal(numberOfLines, layout.TextLines.Count); + + Assert.Equal(numberOfLines * lineHeight, layout.Bounds.Height); + } + } + public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), + .With(renderInterface: new PlatformRenderInterface(null), textShaperImpl: new TextShaperImpl(), - fontManagerImpl : new CustomFontManagerImpl())); + fontManagerImpl: new CustomFontManagerImpl())); return disposable; } From 95f78bbd3f52e1991777bd9e84401dd5f11994b9 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 20 Mar 2020 22:00:06 +0100 Subject: [PATCH 13/13] Include last line when constraint is surpassed --- .../TextLayoutTests.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs index 15ad51ec47..1f89a5833c 100644 --- a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs @@ -29,10 +29,10 @@ namespace Avalonia.Skia.UnitTests var layout = new TextLayout( s_multiLineText, - Typeface.Default, + Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides: spans); + textStyleOverrides : spans); var textLine = layout.TextLines[0]; @@ -72,16 +72,16 @@ namespace Avalonia.Skia.UnitTests 12.0f, Brushes.Black.ToImmutable(), textWrapping: TextWrapping.Wrap, - maxWidth: 25); + maxWidth : 25); var actual = new TextLayout( s_multiLineText, Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textWrapping: TextWrapping.Wrap, - maxWidth: 25, - textStyleOverrides: spans); + textWrapping : TextWrapping.Wrap, + maxWidth : 25, + textStyleOverrides : spans); Assert.Equal(expected.TextLines.Count, actual.TextLines.Count); @@ -115,7 +115,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides: spans); + textStyleOverrides : spans); var textLine = layout.TextLines[0]; @@ -153,7 +153,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides: spans); + textStyleOverrides : spans); var textLine = layout.TextLines[0]; @@ -190,7 +190,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides: spans); + textStyleOverrides : spans); var textLine = layout.TextLines[0]; @@ -301,8 +301,8 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textWrapping: TextWrapping.Wrap, - maxWidth: 180, + textWrapping : TextWrapping.Wrap, + maxWidth : 180, textStyleOverrides: spans); Assert.Equal( @@ -332,8 +332,8 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - maxWidth: 200, - maxHeight: 125, + maxWidth : 200, + maxHeight : 125, textStyleOverrides: spans); Assert.Equal(foreground, layout.TextLines[0].TextRuns[1].Style.Foreground); @@ -430,7 +430,7 @@ namespace Avalonia.Skia.UnitTests Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]); - if (expectedLength == 7) + if(expectedLength == 7) { Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]); } @@ -509,9 +509,9 @@ namespace Avalonia.Skia.UnitTests public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), + .With(renderInterface: new PlatformRenderInterface(null), textShaperImpl: new TextShaperImpl(), - fontManagerImpl: new CustomFontManagerImpl())); + fontManagerImpl : new CustomFontManagerImpl())); return disposable; }