Browse Source

Merge branch 'master' into scrollbar-delinq

pull/3676/head
Dariusz Komosiński 6 years ago
committed by GitHub
parent
commit
fd34436c1e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      native/Avalonia.Native/src/OSX/window.mm
  2. 35
      readme.md
  3. 26
      src/Avalonia.Controls/Application.cs
  4. 20
      src/Avalonia.Controls/TopLevel.cs
  5. 7
      src/Avalonia.Layout/Layoutable.cs
  6. 139
      src/Avalonia.Styling/StyledElement.cs
  7. 14
      src/Avalonia.Styling/Styling/IGlobalStyles.cs
  8. 8
      src/Avalonia.Styling/Styling/IStyle.cs
  9. 16
      src/Avalonia.Styling/Styling/IStyleHost.cs
  10. 11
      src/Avalonia.Styling/Styling/IStyleable.cs
  11. 2
      src/Avalonia.Styling/Styling/Style.cs
  12. 143
      src/Avalonia.Styling/Styling/Styles.cs
  13. 27
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  14. 23
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  15. 39
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
  16. 11
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs
  17. 26
      tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
  18. 179
      tests/Avalonia.Styling.UnitTests/StyleTests.cs

3
native/Avalonia.Native/src/OSX/window.mm

@ -426,6 +426,7 @@ private:
ComPtr<IAvnWindowEvents> 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);
}
}

35
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 |
|---|---|---|
| <a href='https://youtu.be/wHcB3sGLVYg'><img width='300' src='http://avaloniaui.net/images/screen.png'></a> | <a href='https://www.youtube.com/watch?t=28&v=c_AB_XSILp0' target='_blank'><img width='300' src='http://avaloniaui.net/images/avalonia-video.png'></a> | <a href='https://www.youtube.com/watch?v=NJ9-hnmUbBM' target='_blank'><img width='300' src='https://i.ytimg.com/vi/NJ9-hnmUbBM/hqdefault.jpg'></a> |
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 (<a href="http://avaloniaui.net/docs/quickstart/images/new-project-dialog.png">screenshot</a>). 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 <b>NuGet</b> package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/)
Avalonia is delivered via <b>NuGet</b> 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)].
<a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
### Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)]
<a href="https://opencollective.com/Avalonia#backers" target="_blank"><img src="https://opencollective.com/Avalonia/backers.svg?width=890"></a>
### 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)]

26
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<IReadOnlyList<IStyle>> _stylesAdded;
private Action<IReadOnlyList<IStyle>> _stylesRemoved;
/// <summary>
/// Defines the <see cref="DataContext"/> property.
@ -201,6 +204,18 @@ namespace Avalonia
/// </summary>
public IApplicationLifetime ApplicationLifetime { get; set; }
event Action<IReadOnlyList<IStyle>> IGlobalStyles.GlobalStylesAdded
{
add => _stylesAdded += value;
remove => _stylesAdded -= value;
}
event Action<IReadOnlyList<IStyle>> IGlobalStyles.GlobalStylesRemoved
{
add => _stylesRemoved += value;
remove => _stylesRemoved -= value;
}
/// <summary>
/// Initializes the application by loading XAML etc.
/// </summary>
@ -214,6 +229,16 @@ namespace Avalonia
Styles.TryGetResource(key, out value);
}
void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles)
{
_stylesAdded?.Invoke(styles);
}
void IStyleHost.StylesRemoved(IReadOnlyList<IStyle> styles)
{
_stylesRemoved?.Invoke(styles);
}
/// <summary>
/// Register's the services needed by Avalonia.
/// </summary>
@ -286,6 +311,5 @@ namespace Avalonia
get => _name;
set => SetAndRaise(NameProperty, ref _name, value);
}
}
}

20
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<IInputManager>(dependencyResolver);
_keyboardNavigationHandler = TryGetService<IKeyboardNavigationHandler>(dependencyResolver);
_renderInterface = TryGetService<IPlatformRenderInterface>(dependencyResolver);
_globalStyles = TryGetService<IGlobalStyles>(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
/// <inheritdoc/>
double IRenderRoot.RenderScaling => PlatformImpl?.Scaling ?? 1;
IStyleHost IStyleHost.StylingParent
{
get { return AvaloniaLocator.Current.GetService<IGlobalStyles>(); }
}
IStyleHost IStyleHost.StylingParent => _globalStyles;
IRenderTarget IRenderRoot.CreateRenderTarget() => CreateRenderTarget();
@ -267,6 +273,12 @@ namespace Avalonia.Controls
/// </summary>
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);

7
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();
}
/// <inheritdoc/>
protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
{

139
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)
/// <summary>
/// Applies styling to the control if the control is initialized and styling is not
/// already applied.
/// </summary>
/// <returns>
/// A value indicating whether styling is now applied to the control.
/// </returns>
protected bool ApplyStyling()
{
if (_initCount == 0 && (!_styled || force))
if (_initCount == 0 && !_styled)
{
ApplyStyling();
AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
_styled = true;
}
return _styled;
}
/// <summary>
/// Detaches all styles from the element and queues a restyle.
/// </summary>
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<IStyle> styles) => DetachStyles(styles);
void IStyleable.InvalidateStyles() => InvalidateStyles();
void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles)
{
InvalidateStylesOnThisAndDescendents();
}
void IStyleHost.StylesRemoved(IReadOnlyList<IStyle> 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<IStyler>()?.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<IStyle> 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<IStyle> 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<ILogical> children)
@ -750,5 +832,40 @@ namespace Avalonia
{
NotifyResourcesChanged(e);
}
private static IReadOnlyList<IStyle> RecurseStyles(IReadOnlyList<IStyle> styles)
{
var count = styles.Count;
List<IStyle>? 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<IStyle>(styles);
}
RecurseStyles(style.Children, result);
}
}
return result ?? styles;
}
private static void RecurseStyles(IReadOnlyList<IStyle> styles, List<IStyle> result)
{
var count = styles.Count;
for (var i = 0; i < count; ++i)
{
var style = styles[i];
result.Add(style);
RecurseStyles(style.Children, result);
}
}
}
}

14
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
{
/// <summary>
@ -8,5 +13,14 @@ namespace Avalonia.Styling
/// </summary>
public interface IGlobalStyles : IStyleHost
{
/// <summary>
/// Raised when styles are added to <see cref="Styles"/> or a nested styles collection.
/// </summary>
public event Action<IReadOnlyList<IStyle>> GlobalStylesAdded;
/// <summary>
/// Raised when styles are removed from <see cref="Styles"/> or a nested styles collection.
/// </summary>
public event Action<IReadOnlyList<IStyle>> GlobalStylesRemoved;
}
}

8
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
{
/// <summary>
/// Attaches the style to a control if the style's selector matches.
/// Gets a collection of child styles.
/// </summary>
IReadOnlyList<IStyle> Children { get; }
/// <summary>
/// Attaches the style and any child styles to a control if the style's selector matches.
/// </summary>
/// <param name="target">The control to attach to.</param>
/// <param name="host">The element that hosts the style.</param>

16
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
{
/// <summary>
@ -27,5 +31,17 @@ namespace Avalonia.Styling
/// Gets the parent style host element.
/// </summary>
IStyleHost StylingParent { get; }
/// <summary>
/// Called when styles are added to <see cref="Styles"/> or a nested styles collection.
/// </summary>
/// <param name="styles">The added styles.</param>
void StylesAdded(IReadOnlyList<IStyle> styles);
/// <summary>
/// Called when styles are removed from <see cref="Styles"/> or a nested styles collection.
/// </summary>
/// <param name="styles">The removed styles.</param>
void StylesRemoved(IReadOnlyList<IStyle> styles);
}
}

11
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.
/// </summary>
void DetachStyles();
/// <summary>
/// Detaches a collection of styles, if applied to the element.
/// </summary>
void DetachStyles(IReadOnlyList<IStyle> styles);
/// <summary>
/// Detaches all styles from the element and queues a restyle.
/// </summary>
void InvalidateStyles();
}
}

2
src/Avalonia.Styling/Styling/Style.cs

@ -90,6 +90,8 @@ namespace Avalonia.Styling
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0;
IReadOnlyList<IStyle> IStyle.Children => Array.Empty<IStyle>();
/// <inheritdoc/>
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{

143
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;
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
@ -121,6 +84,8 @@ namespace Avalonia.Styling
/// <inheritdoc/>
IStyle IReadOnlyList<IStyle>.this[int index] => _styles[index];
IReadOnlyList<IStyle> IStyle.Children => this;
/// <inheritdoc/>
public IStyle this[int index]
{
@ -257,6 +222,106 @@ namespace Avalonia.Styling
NotifyResourcesChanged(e);
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
static IReadOnlyList<T> ToReadOnlyList<T>(IList list)
{
if (list is IReadOnlyList<T>)
{
return (IReadOnlyList<T>)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<IStyle>(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<IStyle>(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);

27
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);

23
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
{
/// <summary>
@ -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;
/// <summary>
/// Initializes a new instance of the <see cref="StyleInclude"/> class.
@ -41,7 +43,7 @@ namespace Avalonia.Markup.Xaml.Styling
/// <summary>
/// Gets or sets the source URL.
/// </summary>
public Uri Source { get; set; }
public Uri? Source { get; set; }
/// <summary>
/// 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;
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => _parent;
IResourceNode? IResourceNode.ResourceParent => _parent;
IReadOnlyList<IStyle> IStyle.Children => _loaded ?? Array.Empty<IStyle>();
/// <inheritdoc/>
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost host) => Loaded.TryAttach(target, host);
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
/// <inheritdoc/>
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);
/// <inheritdoc/>
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)

39
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<ITopLevelImpl>();
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<Border>().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<TestTopLevel> CreateTemplate()
{
return new FuncControlTemplate<TestTopLevel>((x, scope) =>

11
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<IStyle> styles)
{
throw new NotImplementedException();
}
public void InvalidateStyles()
{
throw new NotImplementedException();
}
public void StyleApplied(IStyleInstance instance)
{
throw new NotImplementedException();

26
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

179
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<Border>())
{
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<Border>())
{
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<Border>())
{
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<Border>())
{
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<Border>())
{
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<Border>())
{
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<Border>())
{
Setters =
{
new Setter(Border.BorderThicknessProperty, new Thickness(4)),
}
},
new Style(x => x.OfType<Border>())
{
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<string> FooProperty =

Loading…
Cancel
Save