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); } } 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)] diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index d7b1aec0b7..f8838e8cfb 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; @@ -45,6 +46,8 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; private bool _notifyingResourcesChanged; + private Action> _stylesAdded; + private Action> _stylesRemoved; /// /// Defines the property. @@ -201,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. /// @@ -214,6 +229,16 @@ namespace Avalonia Styles.TryGetResource(key, out value); } + void IStyleHost.StylesAdded(IReadOnlyList styles) + { + _stylesAdded?.Invoke(styles); + } + + void IStyleHost.StylesRemoved(IReadOnlyList styles) + { + _stylesRemoved?.Invoke(styles); + } + /// /// Register's the services needed by Avalonia. /// @@ -286,6 +311,5 @@ namespace Avalonia get => _name; set => SetAndRaise(NameProperty, ref _name, value); } - } } 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.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 84df9d4b94..241ca1d0ec 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; @@ -352,21 +353,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 +494,21 @@ 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) + { + var allStyles = RecurseStyles(styles); + DetachStylesFromThisAndDescendents(allStyles); + } + protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -604,11 +633,6 @@ namespace Avalonia return null; } - private void ApplyStyling() - { - AvaloniaLocator.Current.GetService()?.ApplyStyles(this); - } - private static void ValidateLogicalChild(ILogical c) { if (c == null) @@ -635,7 +659,7 @@ namespace Avalonia { _logicalRoot = e.Root; - InitializeStylesIfNeeded(true); + ApplyStyling(); OnAttachedToLogicalTree(e); AttachedToLogicalTree?.Invoke(this, e); @@ -713,6 +737,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) @@ -750,5 +832,40 @@ 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; + + 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/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/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 fa722aeb41..182662b32e 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 or a nested styles collection. + /// + /// The added styles. + void StylesAdded(IReadOnlyList styles); + + /// + /// 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/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/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 05cbba5ad2..c9278339b5 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; @@ -121,6 +84,8 @@ namespace Avalonia.Styling /// IStyle IReadOnlyList.this[int index] => _styles[index]; + IReadOnlyList IStyle.Children => this; + /// public IStyle this[int index] { @@ -257,6 +222,106 @@ 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; + } + + GetHost()?.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; + } + + GetHost()?.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 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/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/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.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) => 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(); diff --git a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs index 702e2118f5..1f89a5833c 100644 --- a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs @@ -480,6 +480,32 @@ 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 diff --git a/tests/Avalonia.Styling.UnitTests/StyleTests.cs b/tests/Avalonia.Styling.UnitTests/StyleTests.cs index 4c9b18c886..619f72aab0 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyleTests.cs @@ -244,6 +244,185 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(new Thickness(0), border.BorderThickness); } + [Fact] + public void Removing_Style_Should_Detach_From_Control() + { + 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 Adding_Style_Should_Attach_To_Control() + { + 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); + } + } + + [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 =