diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 7c403e1b04..4eca8a3c25 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -6,6 +6,8 @@ using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.VisualTree; @@ -22,22 +24,25 @@ namespace Avalonia.Diagnostics.ViewModels Type = visual.GetType().Name; Visual = visual; - if (visual is IStyleable styleable) + if (visual is IControl control) { + var removed = Observable.FromEventPattern( + x => control.DetachedFromLogicalTree += x, + x => control.DetachedFromLogicalTree -= x); var classesChanged = Observable.FromEventPattern< NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>( - x => styleable.Classes.CollectionChanged += x, - x => styleable.Classes.CollectionChanged -= x) - .TakeUntil(((IStyleable)styleable).StyleDetach); + x => control.Classes.CollectionChanged += x, + x => control.Classes.CollectionChanged -= x) + .TakeUntil(removed); classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) .Subscribe(_ => { - if (styleable.Classes.Count > 0) + if (control.Classes.Count > 0) { - Classes = "(" + string.Join(" ", styleable.Classes) + ")"; + Classes = "(" + string.Join(" ", control.Classes) + ")"; } else { diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj index a396cee35f..b4f6c2c942 100644 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ b/src/Avalonia.Styling/Avalonia.Styling.csproj @@ -8,5 +8,4 @@ - diff --git a/src/Avalonia.Styling/Controls/NameScopeLocator.cs b/src/Avalonia.Styling/Controls/NameScopeLocator.cs index 354ed33657..51f4c5c4eb 100644 --- a/src/Avalonia.Styling/Controls/NameScopeLocator.cs +++ b/src/Avalonia.Styling/Controls/NameScopeLocator.cs @@ -1,11 +1,5 @@ using System; -using System.Linq; using System.Reactive.Disposables; -using System.Reactive.Threading.Tasks; -using System.Reflection; -using System.Threading.Tasks; -using Avalonia.LogicalTree; -using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Controls diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 120a53c664..aeb3b5dc53 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; -using System.Reactive.Subjects; using Avalonia.Animation; using Avalonia.Collections; using Avalonia.Controls; @@ -14,6 +12,8 @@ using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Styling; +#nullable enable + namespace Avalonia { /// @@ -29,8 +29,8 @@ namespace Avalonia /// /// Defines the property. /// - public static readonly StyledProperty DataContextProperty = - AvaloniaProperty.Register( + public static readonly StyledProperty DataContextProperty = + AvaloniaProperty.Register( nameof(DataContext), inherits: true, notifying: DataContextNotifying); @@ -38,34 +38,34 @@ namespace Avalonia /// /// Defines the property. /// - public static readonly DirectProperty NameProperty = - AvaloniaProperty.RegisterDirect(nameof(Name), o => o.Name, (o, v) => o.Name = v); + public static readonly DirectProperty NameProperty = + AvaloniaProperty.RegisterDirect(nameof(Name), o => o.Name, (o, v) => o.Name = v); /// /// Defines the property. /// - public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); + public static readonly DirectProperty ParentProperty = + AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); /// /// Defines the property. /// - public static readonly DirectProperty TemplatedParentProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty TemplatedParentProperty = + AvaloniaProperty.RegisterDirect( nameof(TemplatedParent), o => o.TemplatedParent, (o ,v) => o.TemplatedParent = v); private int _initCount; - private string _name; + private string? _name; private readonly Classes _classes = new Classes(); - private ILogicalRoot _logicalRoot; - private IAvaloniaList _logicalChildren; - private IResourceDictionary _resources; - private Styles _styles; + private ILogicalRoot? _logicalRoot; + private IAvaloniaList? _logicalChildren; + private IResourceDictionary? _resources; + private Styles? _styles; private bool _styled; - private Subject _styleDetach = new Subject(); - private ITemplatedControl _templatedParent; + private List? _appliedStyles; + private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; /// @@ -87,12 +87,12 @@ namespace Avalonia /// /// Raised when the styled element is attached to a rooted logical tree. /// - public event EventHandler AttachedToLogicalTree; + public event EventHandler? AttachedToLogicalTree; /// /// Raised when the styled element is detached from a rooted logical tree. /// - public event EventHandler DetachedFromLogicalTree; + public event EventHandler? DetachedFromLogicalTree; /// /// Occurs when the property changes. @@ -101,7 +101,7 @@ namespace Avalonia /// This event will be raised when the property has changed and /// all subscribers to that change have been notified. /// - public event EventHandler DataContextChanged; + public event EventHandler? DataContextChanged; /// /// Occurs when the styled element has finished initialization. @@ -114,12 +114,12 @@ namespace Avalonia /// is not used, it is called when the styled element is attached /// to the visual tree. /// - public event EventHandler Initialized; + public event EventHandler? Initialized; /// /// Occurs when a resource in this styled element or a parent styled element has changed. /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; /// /// Gets or sets the name of the styled element. @@ -128,20 +128,12 @@ namespace Avalonia /// An element's name is used to uniquely identify an element within the element's name /// scope. Once the element is added to a logical tree, its name cannot be changed. /// - public string Name + public string? Name { - get - { - return _name; - } + get => _name; set { - if (String.IsNullOrWhiteSpace(value)) - { - throw new InvalidOperationException("Cannot set Name to null or empty string."); - } - if (_styled) { throw new InvalidOperationException("Cannot set Name : styled element already styled."); @@ -189,7 +181,7 @@ namespace Avalonia /// The data context is an inherited property that specifies the default object that will /// be used for data binding. /// - public object DataContext + public object? DataContext { get { return GetValue(DataContextProperty); } set { SetValue(DataContextProperty, value); } @@ -214,28 +206,15 @@ namespace Avalonia /// public Styles Styles { - get { return _styles ?? (Styles = new Styles()); } - set + get { - Contract.Requires(value != null); - - if (_styles != value) + if (_styles is null) { - if (_styles != null) - { - (_styles as ISetResourceParent)?.SetParent(null); - _styles.ResourcesChanged -= ThisResourcesChanged; - } - - _styles = value; - - if (value is ISetResourceParent setParent && setParent.ResourceParent == null) - { - setParent.SetParent(this); - } - + _styles = new Styles(this); _styles.ResourcesChanged += ThisResourcesChanged; } + + return _styles; } } @@ -247,7 +226,7 @@ namespace Avalonia get => _resources ?? (Resources = new ResourceDictionary()); set { - Contract.Requires(value != null); + value = value ?? throw new ArgumentNullException(nameof(value)); var hadResources = false; @@ -270,7 +249,7 @@ namespace Avalonia /// /// Gets the styled element whose lookless template this styled element is part of. /// - public ITemplatedControl TemplatedParent + public ITemplatedControl? TemplatedParent { get => _templatedParent; internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value); @@ -312,12 +291,12 @@ namespace Avalonia /// /// Gets the styled element's logical parent. /// - public IStyledElement Parent { get; private set; } + public IStyledElement? Parent { get; private set; } /// /// Gets the styled element's logical parent. /// - ILogical ILogical.LogicalParent => Parent; + ILogical? ILogical.LogicalParent => Parent; /// /// Gets the styled element's logical children. @@ -328,7 +307,7 @@ namespace Avalonia bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; /// - IResourceNode IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; + IResourceNode? IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; /// IAvaloniaReadOnlyList IStyleable.Classes => Classes; @@ -344,14 +323,11 @@ namespace Avalonia /// Type IStyleable.StyleKey => GetType(); - /// - IObservable IStyleable.StyleDetach => _styleDetach; - /// bool IStyleHost.IsStylesInitialized => _styles != null; /// - IStyleHost IStyleHost.StylingParent => (IStyleHost)InheritanceParent; + IStyleHost? IStyleHost.StylingParent => (IStyleHost)InheritanceParent; /// public virtual void BeginInit() @@ -397,13 +373,13 @@ namespace Avalonia /// void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { - this.OnAttachedToLogicalTreeCore(e); + OnAttachedToLogicalTreeCore(e); } /// void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { - this.OnDetachedFromLogicalTreeCore(e); + OnDetachedFromLogicalTreeCore(e); } /// @@ -413,7 +389,7 @@ namespace Avalonia } /// - bool IResourceProvider.TryGetResource(object key, out object value) + bool IResourceProvider.TryGetResource(object key, out object? value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || @@ -424,7 +400,7 @@ namespace Avalonia /// Sets the styled element's logical parent. /// /// The parent. - void ISetLogicalParent.SetParent(ILogical parent) + void ISetLogicalParent.SetParent(ILogical? parent) { var old = Parent; @@ -440,7 +416,7 @@ namespace Avalonia InheritanceParent = parent as AvaloniaObject; } - Parent = (IStyledElement)parent; + Parent = (IStyledElement?)parent; if (_logicalRoot != null) { @@ -470,12 +446,13 @@ namespace Avalonia var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent); OnAttachedToLogicalTreeCore(e); } - +#nullable disable RaisePropertyChanged( ParentProperty, new Optional(old), new BindingValue(Parent), BindingPriority.LocalValue); +#nullable enable } } @@ -488,6 +465,16 @@ namespace Avalonia InheritanceParent = parent; } + void IStyleable.StyleApplied(IStyleInstance instance) + { + instance = instance ?? throw new ArgumentNullException(nameof(instance)); + + _appliedStyles ??= new List(); + _appliedStyles.Add(instance); + } + + void IStyleable.DetachStyles() => DetachStyles(); + protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -597,7 +584,7 @@ namespace Avalonia } } - private static ILogicalRoot FindLogicalRoot(IStyleHost e) + private static ILogicalRoot? FindLogicalRoot(IStyleHost e) { while (e != null) { @@ -666,7 +653,7 @@ namespace Avalonia if (_logicalRoot != null) { _logicalRoot = null; - _styleDetach.OnNext(this); + DetachStyles(); OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); @@ -682,7 +669,7 @@ namespace Avalonia } #if DEBUG - if (((INotifyCollectionChangedDebug)_classes).GetCollectionChangedSubscribers()?.Length > 0) + if (((INotifyCollectionChangedDebug)Classes).GetCollectionChangedSubscribers()?.Length > 0) { Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Control, @@ -710,6 +697,19 @@ namespace Avalonia } } + private void DetachStyles() + { + if (_appliedStyles is object) + { + foreach (var i in _appliedStyles) + { + i.Dispose(); + } + + _appliedStyles.Clear(); + } + } + private void ClearLogicalParent(IEnumerable children) { foreach (var i in children) diff --git a/src/Avalonia.Styling/Styling/ActivatedObservable.cs b/src/Avalonia.Styling/Styling/ActivatedObservable.cs deleted file mode 100644 index 5b2774943a..0000000000 --- a/src/Avalonia.Styling/Styling/ActivatedObservable.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; - -namespace Avalonia.Styling -{ - /// - /// An observable which is switched on or off according to an activator observable. - /// - /// - /// An has two inputs: an activator observable and a - /// observable which produces the activated value. When the activator - /// produces true, the will produce the current activated - /// value. When the activator produces false it will produce - /// . - /// - internal class ActivatedObservable : ActivatedValue, IDescription - { - private IDisposable _sourceSubscription; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// An observable that produces the activated value. - /// The binding description. - public ActivatedObservable( - IObservable activator, - IObservable source, - string description) - : base(activator, AvaloniaProperty.UnsetValue, description) - { - Contract.Requires(source != null); - - Source = source; - } - - /// - /// Gets an observable which produces the . - /// - public IObservable Source { get; } - - protected override ActivatorListener CreateListener() => new ValueListener(this); - - protected override void Deinitialize() - { - base.Deinitialize(); - _sourceSubscription.Dispose(); - _sourceSubscription = null; - } - - protected override void Initialize() - { - base.Initialize(); - _sourceSubscription = Source.Subscribe((ValueListener)Listener); - } - - protected virtual void NotifyValue(object value) - { - Value = value; - } - - private class ValueListener : ActivatorListener, IObserver - { - public ValueListener(ActivatedObservable parent) - : base(parent) - { - } - protected new ActivatedObservable Parent => (ActivatedObservable)base.Parent; - - void IObserver.OnCompleted() => Parent.CompletedReceived(); - void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); - void IObserver.OnNext(object value) => Parent.NotifyValue(value); - } - } -} diff --git a/src/Avalonia.Styling/Styling/ActivatedSubject.cs b/src/Avalonia.Styling/Styling/ActivatedSubject.cs deleted file mode 100644 index a8446c4bfb..0000000000 --- a/src/Avalonia.Styling/Styling/ActivatedSubject.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive.Subjects; - -namespace Avalonia.Styling -{ - /// - /// A subject which is switched on or off according to an activator observable. - /// - /// - /// An extends to - /// be an . When the object is active then values - /// received via will be passed to the source subject. - /// - internal class ActivatedSubject : ActivatedObservable, ISubject, IDescription - { - private bool _completed; - private object _pushValue; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// An observable that produces the activated value. - /// The binding description. - public ActivatedSubject( - IObservable activator, - ISubject source, - string description) - : base(activator, source, description) - { - } - - /// - /// Gets the underlying subject. - /// - public new ISubject Source - { - get { return (ISubject)base.Source; } - } - - public void OnCompleted() - { - Source.OnCompleted(); - } - - public void OnError(Exception error) - { - Source.OnError(error); - } - - public void OnNext(object value) - { - _pushValue = value; - - if (IsActive == true && !_completed) - { - Source.OnNext(_pushValue); - } - } - - protected override void ActiveChanged(bool active) - { - bool first = !IsActive.HasValue; - - base.ActiveChanged(active); - - if (!first) - { - Source.OnNext(active ? _pushValue : AvaloniaProperty.UnsetValue); - } - } - - protected override void CompletedReceived() - { - base.CompletedReceived(); - - if (!_completed) - { - Source.OnCompleted(); - _completed = true; - } - } - - protected override void ErrorReceived(Exception error) - { - base.ErrorReceived(error); - - if (!_completed) - { - Source.OnError(error); - _completed = true; - } - } - - private void ActivatorCompleted() - { - _completed = true; - Source.OnCompleted(); - } - - private void ActivatorError(Exception e) - { - _completed = true; - Source.OnError(e); - } - } -} diff --git a/src/Avalonia.Styling/Styling/ActivatedValue.cs b/src/Avalonia.Styling/Styling/ActivatedValue.cs deleted file mode 100644 index 908d89b751..0000000000 --- a/src/Avalonia.Styling/Styling/ActivatedValue.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Avalonia.Reactive; - -namespace Avalonia.Styling -{ - /// - /// An value which is switched on or off according to an activator observable. - /// - /// - /// An has two inputs: an activator observable and an - /// . When the activator produces true, the - /// will produce the current value. When the activator - /// produces false it will produce . - /// - internal class ActivatedValue : LightweightObservableBase, IDescription - { - private static readonly object NotSent = new object(); - private IDisposable _activatorSubscription; - private object _value; - private object _last = NotSent; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// The activated value. - /// The binding description. - public ActivatedValue( - IObservable activator, - object value, - string description) - { - Contract.Requires(activator != null); - - Activator = activator; - Value = value; - Description = description; - Listener = CreateListener(); - } - - /// - /// Gets the activator observable. - /// - public IObservable Activator { get; } - - /// - /// Gets a description of the binding. - /// - public string Description { get; } - - /// - /// Gets a value indicating whether the activator is active. - /// - public bool? IsActive { get; private set; } - - /// - /// Gets the value that will be produced when is true. - /// - public object Value - { - get => _value; - protected set - { - _value = value; - PublishValue(); - } - } - - protected ActivatorListener Listener { get; } - - protected virtual void ActiveChanged(bool active) - { - IsActive = active; - PublishValue(); - } - - protected virtual void CompletedReceived() => PublishCompleted(); - - protected virtual ActivatorListener CreateListener() => new ActivatorListener(this); - - protected override void Deinitialize() - { - _activatorSubscription.Dispose(); - _activatorSubscription = null; - } - - protected virtual void ErrorReceived(Exception error) => PublishError(error); - - protected override void Initialize() - { - _activatorSubscription = Activator.Subscribe(Listener); - } - - protected override void Subscribed(IObserver observer, bool first) - { - if (IsActive == true && !first) - { - observer.OnNext(Value); - } - } - - private void PublishValue() - { - if (IsActive.HasValue) - { - var v = IsActive.Value ? Value : AvaloniaProperty.UnsetValue; - - if (!Equals(v, _last)) - { - PublishNext(v); - _last = v; - } - } - } - - protected class ActivatorListener : IObserver - { - public ActivatorListener(ActivatedValue parent) - { - Parent = parent; - } - - protected ActivatedValue Parent { get; } - - void IObserver.OnCompleted() => Parent.CompletedReceived(); - void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); - void IObserver.OnNext(bool value) => Parent.ActiveChanged(value); - } - } -} diff --git a/src/Avalonia.Styling/Styling/Activators/AndActivator.cs b/src/Avalonia.Styling/Styling/Activators/AndActivator.cs new file mode 100644 index 0000000000..8ab281e8d0 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/AndActivator.cs @@ -0,0 +1,67 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Avalonia.Styling.Activators +{ + internal class AndActivator : StyleActivatorBase, IStyleActivatorSink + { + private List? _sources; + private ulong _flags; + private ulong _mask; + + public int Count => _sources?.Count ?? 0; + + public void Add(IStyleActivator activator) + { + _sources ??= new List(); + _sources.Add(activator); + } + + void IStyleActivatorSink.OnNext(bool value, int tag) + { + if (value) + { + _flags |= 1ul << tag; + } + else + { + _flags &= ~(1ul << tag); + } + + if (_mask != 0) + { + PublishNext(_flags == _mask); + } + } + + protected override void Initialize() + { + if (_sources is object) + { + var i = 0; + + foreach (var source in _sources) + { + source.Subscribe(this, i++); + } + + _mask = (1ul << Count) - 1; + PublishNext(_flags == _mask); + } + } + + protected override void Deinitialize() + { + if (_sources is object) + { + foreach (var source in _sources) + { + source.Unsubscribe(this); + } + } + + _mask = 0; + } + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs b/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs new file mode 100644 index 0000000000..479100ed8a --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs @@ -0,0 +1,33 @@ +#nullable enable + +using System; + +namespace Avalonia.Styling.Activators +{ + /// + /// Defines a style activator. + /// + /// + /// A style activator is very similar to an `IObservable{bool}` but is optimized for the + /// particular use-case of activating a style according to a selector. It differs from + /// an observable in two major ways: + /// + /// - Can only have a single subscription + /// - The subscription can have a tag associated with it, allowing a subscriber to index + /// into a list of subscriptions without having to allocate additional objects. + /// + public interface IStyleActivator : IDisposable + { + /// + /// Subscribes to the activator. + /// + /// The listener. + /// An optional tag. + void Subscribe(IStyleActivatorSink sink, int tag = 0); + + /// + /// Unsubscribes from the activator. + /// + void Unsubscribe(IStyleActivatorSink sink); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs b/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs new file mode 100644 index 0000000000..a1a6ef5c28 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs @@ -0,0 +1,17 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + /// + /// Receives notifications from an . + /// + public interface IStyleActivatorSink + { + /// + /// Called when the subscribed activator value changes. + /// + /// The new value. + /// The subscription tag. + void OnNext(bool value, int tag); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/NotActivator.cs b/src/Avalonia.Styling/Styling/Activators/NotActivator.cs new file mode 100644 index 0000000000..4c152a8f0f --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/NotActivator.cs @@ -0,0 +1,13 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal class NotActivator : StyleActivatorBase, IStyleActivatorSink + { + private readonly IStyleActivator _source; + public NotActivator(IStyleActivator source) => _source = source; + void IStyleActivatorSink.OnNext(bool value, int tag) => PublishNext(!value); + protected override void Initialize() => _source.Subscribe(this, 0); + protected override void Deinitialize() => _source.Unsubscribe(this); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/OrActivator.cs b/src/Avalonia.Styling/Styling/Activators/OrActivator.cs new file mode 100644 index 0000000000..0220265e10 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/OrActivator.cs @@ -0,0 +1,67 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Avalonia.Styling.Activators +{ + internal class OrActivator : StyleActivatorBase, IStyleActivatorSink + { + private List? _sources; + private ulong _flags; + private bool _initializing; + + public int Count => _sources?.Count ?? 0; + + public void Add(IStyleActivator activator) + { + _sources ??= new List(); + _sources.Add(activator); + } + + void IStyleActivatorSink.OnNext(bool value, int tag) + { + if (value) + { + _flags |= 1ul << tag; + } + else + { + _flags &= ~(1ul << tag); + } + + if (!_initializing) + { + PublishNext(_flags != 0); + } + } + + protected override void Initialize() + { + if (_sources is object) + { + var i = 0; + + _initializing = true; + + foreach (var source in _sources) + { + source.Subscribe(this, i++); + } + + _initializing = false; + PublishNext(_flags != 0); + } + } + + protected override void Deinitialize() + { + if (_sources is object) + { + foreach (var source in _sources) + { + source.Unsubscribe(this); + } + } + } + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs new file mode 100644 index 0000000000..abf3c1717e --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs @@ -0,0 +1,35 @@ +using System; + +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal class PropertyEqualsActivator : StyleActivatorBase, IObserver + { + private readonly IStyleable _control; + private readonly AvaloniaProperty _property; + private readonly object? _value; + private IDisposable? _subscription; + + public PropertyEqualsActivator( + IStyleable control, + AvaloniaProperty property, + object? value) + { + _control = control ?? throw new ArgumentNullException(nameof(control)); + _property = property ?? throw new ArgumentNullException(nameof(property)); + _value = value; + } + + protected override void Initialize() + { + _subscription = _control.GetObservable(_property).Subscribe(this); + } + + protected override void Deinitialize() => _subscription?.Dispose(); + + void IObserver.OnCompleted() { } + void IObserver.OnError(Exception error) { } + void IObserver.OnNext(object value) => PublishNext(Equals(value, _value)); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs b/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs new file mode 100644 index 0000000000..725547ed05 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs @@ -0,0 +1,55 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal abstract class StyleActivatorBase : IStyleActivator + { + private IStyleActivatorSink? _sink; + private int _tag; + private bool? _value; + + public void Subscribe(IStyleActivatorSink sink, int tag = 0) + { + if (_sink is null) + { + _sink = sink; + _tag = tag; + _value = null; + Initialize(); + } + else + { + throw new AvaloniaInternalException("Cannot subscribe to a StyleActivator more than once."); + } + } + + public void Unsubscribe(IStyleActivatorSink sink) + { + if (_sink != sink) + { + throw new AvaloniaInternalException("StyleActivatorSink is not subscribed."); + } + + _sink = null; + Deinitialize(); + } + + public void PublishNext(bool value) + { + if (_value != value) + { + _value = value; + _sink?.OnNext(value, _tag); + } + } + + public void Dispose() + { + _sink = null; + Deinitialize(); + } + + protected abstract void Initialize(); + protected abstract void Deinitialize(); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs new file mode 100644 index 0000000000..906a8303cb --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Collections; + +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal sealed class StyleClassActivator : StyleActivatorBase + { + private readonly IList _match; + private readonly IAvaloniaReadOnlyList _classes; + + public StyleClassActivator(IAvaloniaReadOnlyList classes, IList match) + { + _classes = classes; + _match = match; + } + + public static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) + { + int remainingMatches = toMatch.Count; + int classesCount = classes.Count; + + // Early bail out - we can't match if control does not have enough classes. + if (classesCount < remainingMatches) + { + return false; + } + + for (var i = 0; i < classesCount; i++) + { + var c = classes[i]; + + if (toMatch.Contains(c)) + { + --remainingMatches; + + // Already matched so we can skip checking other classes. + if (remainingMatches == 0) + { + break; + } + } + } + + return remainingMatches == 0; + } + + + protected override void Initialize() + { + PublishNext(IsMatching()); + _classes.CollectionChanged += ClassesChanged; + } + + protected override void Deinitialize() + { + _classes.CollectionChanged -= ClassesChanged; + } + + private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action != NotifyCollectionChangedAction.Move) + { + PublishNext(IsMatching()); + } + } + + private bool IsMatching() => AreClassesMatching(_classes, _match); + } +} diff --git a/src/Avalonia.Styling/Styling/DescendentSelector.cs b/src/Avalonia.Styling/Styling/DescendentSelector.cs index a81908f23d..08b25f4057 100644 --- a/src/Avalonia.Styling/Styling/DescendentSelector.cs +++ b/src/Avalonia.Styling/Styling/DescendentSelector.cs @@ -2,24 +2,21 @@ // 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.LogicalTree; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { internal class DescendantSelector : Selector { private readonly Selector _parent; - private string _selectorString; + private string? _selectorString; - public DescendantSelector(Selector parent) + public DescendantSelector(Selector? parent) { - if (parent == null) - { - throw new InvalidOperationException("Descendant selector must be preceeded by a selector."); - } - - _parent = parent; + _parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceeded by a selector."); } /// @@ -29,7 +26,7 @@ namespace Avalonia.Styling public override bool InTemplate => _parent.InTemplate; /// - public override Type TargetType => null; + public override Type? TargetType => null; public override string ToString() { @@ -43,8 +40,9 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - ILogical c = (ILogical)control; - List> descendantMatches = new List>(); + var c = (ILogical)control; + IStyleActivator? descendentMatch = null; + OrActivator? descendantMatches = null; while (c != null) { @@ -56,7 +54,21 @@ namespace Avalonia.Styling if (match.Result == SelectorMatchResult.Sometimes) { - descendantMatches.Add(match.Activator); + if (descendentMatch is null && descendantMatches is null) + { + descendentMatch = match.Activator; + } + else + { + if (descendantMatches is null) + { + descendantMatches = new OrActivator(); + descendantMatches.Add(descendentMatch!); + descendentMatch = null; + } + + descendantMatches.Add(match.Activator!); + } } else if (match.IsMatch) { @@ -65,9 +77,13 @@ namespace Avalonia.Styling } } - if (descendantMatches.Count > 0) + if (descendantMatches is object) + { + return new SelectorMatch(descendantMatches); + } + else if (descendentMatch is object) { - return new SelectorMatch(StyleActivator.Or(descendantMatches)); + return new SelectorMatch(descendentMatch); } else { @@ -75,6 +91,6 @@ namespace Avalonia.Styling } } - protected override Selector MovePrevious() => null; + protected override Selector? MovePrevious() => null; } } diff --git a/src/Avalonia.Styling/Styling/ISetter.cs b/src/Avalonia.Styling/Styling/ISetter.cs index da97638f07..44e43caf85 100644 --- a/src/Avalonia.Styling/Styling/ISetter.cs +++ b/src/Avalonia.Styling/Styling/ISetter.cs @@ -3,6 +3,8 @@ using System; +#nullable enable + namespace Avalonia.Styling { /// @@ -11,11 +13,16 @@ namespace Avalonia.Styling public interface ISetter { /// - /// Applies the setter to a control. + /// Instances a setter on a control. /// - /// The style that is being applied. - /// The control. - /// An optional activator. - IDisposable Apply(IStyle style, IStyleable control, IObservable activator); + /// The control. + /// Whether the parent style has an activator. + /// An . + /// + /// This method should return an which can be used to apply + /// the setter to the specified control. Note that it should not apply the setter value + /// until is called. + /// + ISetterInstance Instance(IStyleable target, bool hasActivator); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Styling/Styling/ISetterInstance.cs b/src/Avalonia.Styling/Styling/ISetterInstance.cs new file mode 100644 index 0000000000..ebfc227d12 --- /dev/null +++ b/src/Avalonia.Styling/Styling/ISetterInstance.cs @@ -0,0 +1,20 @@ +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// Represents a setter that has been instanced on a control. + /// + public interface ISetterInstance + { + /// + /// Activates the setter. + /// + public void Activate(); + + /// + /// Deactivates the setter. + /// + public void Deactivate(); + } +} diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs index da2a08f04d..8151aacf54 100644 --- a/src/Avalonia.Styling/Styling/IStyle.cs +++ b/src/Avalonia.Styling/Styling/IStyle.cs @@ -3,6 +3,8 @@ using Avalonia.Controls; +#nullable enable + namespace Avalonia.Styling { /// @@ -13,17 +15,11 @@ namespace Avalonia.Styling /// /// Attaches the style to a control if the style's selector matches. /// - /// The control to attach to. - /// - /// The control that contains this style. May be null. - /// + /// The control to attach to. + /// The element that hosts the style. /// - /// True if the style can match a control of type - /// (even if it does not match this control specifically); false if the style - /// can never match. + /// A describing how the style matches the control. /// - bool Attach(IStyleable control, IStyleHost container); - - void Detach(); + SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host); } } diff --git a/src/Avalonia.Styling/Styling/IStyleInstance.cs b/src/Avalonia.Styling/Styling/IStyleInstance.cs new file mode 100644 index 0000000000..cb094badd2 --- /dev/null +++ b/src/Avalonia.Styling/Styling/IStyleInstance.cs @@ -0,0 +1,22 @@ +using System; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// Represents a style that has been instanced on a control. + /// + public interface IStyleInstance : IDisposable + { + /// + /// Gets the source style. + /// + IStyle Source { get; } + + /// + /// Instructs the style to start acting upon the control. + /// + void Start(); + } +} diff --git a/src/Avalonia.Styling/Styling/IStyleable.cs b/src/Avalonia.Styling/Styling/IStyleable.cs index 5ad97d8a61..b01c779bcc 100644 --- a/src/Avalonia.Styling/Styling/IStyleable.cs +++ b/src/Avalonia.Styling/Styling/IStyleable.cs @@ -4,6 +4,8 @@ using System; using Avalonia.Collections; +#nullable enable + namespace Avalonia.Styling { /// @@ -11,11 +13,6 @@ namespace Avalonia.Styling /// public interface IStyleable : IAvaloniaObject, INamed { - /// - /// Signaled when the control's style should be removed. - /// - IObservable StyleDetach { get; } - /// /// Gets the list of classes for the control. /// @@ -29,6 +26,17 @@ namespace Avalonia.Styling /// /// Gets the template parent of this element if the control comes from a template. /// - ITemplatedControl TemplatedParent { get; } + ITemplatedControl? TemplatedParent { get; } + + /// + /// Notifies the element that a style has been applied. + /// + /// The style instance. + void StyleApplied(IStyleInstance instance); + + /// + /// Detaches all styles applied to the element. + /// + void DetachStyles(); } } diff --git a/src/Avalonia.Styling/Styling/NotSelector.cs b/src/Avalonia.Styling/Styling/NotSelector.cs index bcf76620be..6428535a12 100644 --- a/src/Avalonia.Styling/Styling/NotSelector.cs +++ b/src/Avalonia.Styling/Styling/NotSelector.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -11,16 +13,16 @@ namespace Avalonia.Styling /// internal class NotSelector : Selector { - private readonly Selector _previous; + private readonly Selector? _previous; private readonly Selector _argument; - private string _selectorString; + private string? _selectorString; /// /// Initializes a new instance of the class. /// /// The previous selector. /// The selector to be not-ed. - public NotSelector(Selector previous, Selector argument) + public NotSelector(Selector? previous, Selector argument) { _previous = previous; _argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument."); @@ -33,7 +35,7 @@ namespace Avalonia.Styling public override bool IsCombinator => false; /// - public override Type TargetType => _previous?.TargetType; + public override Type? TargetType => _previous?.TargetType; /// public override string ToString() @@ -61,12 +63,12 @@ namespace Avalonia.Styling case SelectorMatchResult.NeverThisType: return SelectorMatch.AlwaysThisType; case SelectorMatchResult.Sometimes: - return new SelectorMatch(innerResult.Activator.Select(x => !x)); + return new SelectorMatch(new NotActivator(innerResult.Activator!)); default: throw new InvalidOperationException("Invalid SelectorMatchResult."); } } - protected override Selector MovePrevious() => _previous; + protected override Selector? MovePrevious() => _previous; } } diff --git a/src/Avalonia.Styling/Styling/OrSelector.cs b/src/Avalonia.Styling/Styling/OrSelector.cs index 58c5c778fb..9c76a38f45 100644 --- a/src/Avalonia.Styling/Styling/OrSelector.cs +++ b/src/Avalonia.Styling/Styling/OrSelector.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Generic; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -12,8 +15,8 @@ namespace Avalonia.Styling internal class OrSelector : Selector { private readonly IReadOnlyList _selectors; - private string _selectorString; - private Type _targetType; + private string? _selectorString; + private Type? _targetType; /// /// Initializes a new instance of the class. @@ -21,8 +24,15 @@ namespace Avalonia.Styling /// The selectors to OR. public OrSelector(IReadOnlyList selectors) { - Contract.Requires(selectors != null); - Contract.Requires(selectors.Count > 1); + if (selectors is null) + { + throw new ArgumentNullException(nameof(selectors)); + } + + if (selectors.Count <= 1) + { + throw new ArgumentException("Need more than one selector to OR."); + } _selectors = selectors; } @@ -34,7 +44,7 @@ namespace Avalonia.Styling public override bool IsCombinator => false; /// - public override Type TargetType + public override Type? TargetType { get { @@ -60,7 +70,8 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - var activators = new List>(); + IStyleActivator? activator = null; + OrActivator? activators = null; var neverThisInstance = false; foreach (var selector in _selectors) @@ -76,18 +87,32 @@ namespace Avalonia.Styling neverThisInstance = true; break; case SelectorMatchResult.Sometimes: - activators.Add(match.Activator); + if (activator is null && activators is null) + { + activator = match.Activator; + } + else + { + if (activators is null) + { + activators = new OrActivator(); + activators.Add(activator!); + activator = null; + } + + activators.Add(match.Activator!); + } break; } } - if (activators.Count > 1) + if (activators is object) { - return new SelectorMatch(StyleActivator.Or(activators)); + return new SelectorMatch(activators); } - else if (activators.Count == 1) + else if (activator is object) { - return new SelectorMatch(activators[0]); + return new SelectorMatch(activator); } else if (neverThisInstance) { @@ -99,11 +124,11 @@ namespace Avalonia.Styling } } - protected override Selector MovePrevious() => null; + protected override Selector? MovePrevious() => null; - private Type EvaluateTargetType() + private Type? EvaluateTargetType() { - var result = default(Type); + Type? result = null; foreach (var selector in _selectors) { diff --git a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs index cfc0998fe0..d7e1f46a94 100644 --- a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; using System.Text; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -13,14 +15,14 @@ namespace Avalonia.Styling /// internal class PropertyEqualsSelector : Selector { - private readonly Selector _previous; + private readonly Selector? _previous; private readonly AvaloniaProperty _property; - private readonly object _value; - private string _selectorString; + private readonly object? _value; + private string? _selectorString; - public PropertyEqualsSelector(Selector previous, AvaloniaProperty property, object value) + public PropertyEqualsSelector(Selector? previous, AvaloniaProperty property, object? value) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); _previous = previous; _property = property; @@ -33,13 +35,8 @@ namespace Avalonia.Styling /// public override bool IsCombinator => false; - /// - /// Gets the name of the control to match. - /// - public string Name { get; private set; } - /// - public override Type TargetType => _previous?.TargetType; + public override Type? TargetType => _previous?.TargetType; /// public override string ToString() @@ -77,7 +74,7 @@ namespace Avalonia.Styling { if (subscribe) { - return new SelectorMatch(control.GetObservable(_property).Select(v => Equals(v ?? string.Empty, _value))); + return new SelectorMatch(new PropertyEqualsActivator(control, _property, _value)); } else { @@ -86,6 +83,6 @@ namespace Avalonia.Styling } } - protected override Selector MovePrevious() => _previous; + protected override Selector? MovePrevious() => _previous; } } diff --git a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs new file mode 100644 index 0000000000..74d7f98398 --- /dev/null +++ b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Styling +{ + internal class PropertySetterBindingInstance : ISetterInstance + { + private readonly IStyleable _target; + private readonly AvaloniaProperty _property; + private readonly BindingPriority _priority; + private readonly InstancedBinding _binding; + private IDisposable? _subscription; + private bool _isActive; + + public PropertySetterBindingInstance( + IStyleable target, + AvaloniaProperty property, + BindingPriority priority, + IBinding binding) + { + _target = target; + _property = property; + _priority = priority; + _binding = binding.Initiate(target, property).WithPriority(priority); + } + + public void Activate() + { + if (!_isActive) + { + _subscription = BindingOperations.Apply(_target, _property, _binding, null); + _isActive = true; + } + } + + public void Deactivate() + { + if (_isActive) + { + _subscription?.Dispose(); + _subscription = null; + _isActive = false; + } + } + } +} diff --git a/src/Avalonia.Styling/Styling/PropertySetterInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs new file mode 100644 index 0000000000..284ca8cdd0 --- /dev/null +++ b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs @@ -0,0 +1,82 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Styling +{ + internal class PropertySetterInstance : ISetterInstance + { + private readonly IStyleable _target; + private readonly StyledPropertyBase? _styledProperty; + private readonly DirectPropertyBase? _directProperty; + private readonly BindingPriority _priority; + private readonly T _value; + private IDisposable? _subscription; + private bool _isActive; + + public PropertySetterInstance( + IStyleable target, + StyledPropertyBase property, + BindingPriority priority, + T value) + { + _target = target; + _styledProperty = property; + _priority = priority; + _value = value; + } + + public PropertySetterInstance( + IStyleable target, + DirectPropertyBase property, + BindingPriority priority, + T value) + { + _target = target; + _directProperty = property; + _priority = priority; + _value = value; + } + + public void Activate() + { + if (!_isActive) + { + if (_styledProperty is object) + { + _subscription = _target.SetValue(_styledProperty, _value, _priority); + } + else + { + _target.SetValue(_directProperty!, _value); + } + + _isActive = true; + } + } + + public void Deactivate() + { + if (_isActive) + { + if (_subscription is null) + { + if (_styledProperty is object) + { + _target.ClearValue(_styledProperty); + } + else + { + _target.ClearValue(_directProperty!); + } + } + else + { + _subscription.Dispose(); + _subscription = null; + } + } + } + } +} diff --git a/src/Avalonia.Styling/Styling/Selector.cs b/src/Avalonia.Styling/Styling/Selector.cs index 7d4e92baeb..6d74eb8842 100644 --- a/src/Avalonia.Styling/Styling/Selector.cs +++ b/src/Avalonia.Styling/Styling/Selector.cs @@ -2,9 +2,9 @@ // 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.Diagnostics; -using Avalonia.Utilities; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -30,7 +30,7 @@ namespace Avalonia.Styling /// /// Gets the target type of the selector, if available. /// - public abstract Type TargetType { get; } + public abstract Type? TargetType { get; } /// /// Tries to match the selector with a control. @@ -43,8 +43,8 @@ namespace Avalonia.Styling /// A . public SelectorMatch Match(IStyleable control, bool subscribe = true) { - ValueSingleOrList> inputs = default; - + IStyleActivator? activator = null; + AndActivator? activators = null; var selector = this; var alwaysThisType = true; var hitCombinator = false; @@ -69,21 +69,39 @@ namespace Avalonia.Styling } else if (match.Result == SelectorMatchResult.Sometimes) { - Debug.Assert(match.Activator != null); + if (match.Activator is null) + { + throw new AvaloniaInternalException( + "SelectorMatch returned Sometimes but there is no activator."); + } + + if (activator is null && activators is null) + { + activator = match.Activator; + } + else + { + if (activators is null) + { + activators = new AndActivator(); + activators.Add(activator!); + activator = null; + } - inputs.Add(match.Activator); + activators.Add(match.Activator); + } } selector = selector.MovePrevious(); } - if (inputs.HasList) + if (activators is object) { - return new SelectorMatch(StyleActivator.And(inputs.List)); + return new SelectorMatch(activators); } - else if (inputs.IsSingle) + else if (activator is object) { - return new SelectorMatch(inputs.Single); + return new SelectorMatch(activator); } else { @@ -107,6 +125,6 @@ namespace Avalonia.Styling /// /// Moves to the previous selector. /// - protected abstract Selector MovePrevious(); + protected abstract Selector? MovePrevious(); } } diff --git a/src/Avalonia.Styling/Styling/SelectorMatch.cs b/src/Avalonia.Styling/Styling/SelectorMatch.cs index 63b89e9e97..3cc84a0b57 100644 --- a/src/Avalonia.Styling/Styling/SelectorMatch.cs +++ b/src/Avalonia.Styling/Styling/SelectorMatch.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -21,9 +24,9 @@ namespace Avalonia.Styling NeverThisInstance, /// - /// The selector always matches this type. + /// The selector matches this instance based on the . /// - AlwaysThisType, + Sometimes, /// /// The selector always matches this instance, but doesn't always match this type. @@ -31,9 +34,9 @@ namespace Avalonia.Styling AlwaysThisInstance, /// - /// The selector matches this instance based on the . + /// The selector always matches this type. /// - Sometimes, + AlwaysThisType, } /// @@ -43,7 +46,7 @@ namespace Avalonia.Styling /// A selector match describes whether and how a matches a control, and /// in addition whether the selector can ever match a control of the same type. /// - public class SelectorMatch + public readonly struct SelectorMatch { /// /// A selector match with the result of . @@ -70,20 +73,24 @@ namespace Avalonia.Styling /// result. /// /// The match activator. - public SelectorMatch(IObservable match) + public SelectorMatch(IStyleActivator match) { - Contract.Requires(match != null); + match = match ?? throw new ArgumentNullException(nameof(match)); Result = SelectorMatchResult.Sometimes; Activator = match; } - private SelectorMatch(SelectorMatchResult result) => Result = result; + private SelectorMatch(SelectorMatchResult result) + { + Result = result; + Activator = null; + } /// /// Gets a value indicating whether the match was positive. /// - public bool IsMatch => Result >= SelectorMatchResult.AlwaysThisType; + public bool IsMatch => Result >= SelectorMatchResult.Sometimes; /// /// Gets the result of the match. @@ -91,9 +98,9 @@ namespace Avalonia.Styling public SelectorMatchResult Result { get; } /// - /// Gets an observable which tracks the selector match, in the case of selectors that can + /// Gets an activator which tracks the selector match, in the case of selectors that can /// change over time. /// - public IObservable Activator { get; } + public IStyleActivator? Activator { get; } } } diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index b880ecb01c..08e8a699b8 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -2,11 +2,12 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Disposables; using Avalonia.Animation; using Avalonia.Data; using Avalonia.Metadata; -using Avalonia.Reactive; +using Avalonia.Utilities; + +#nullable enable namespace Avalonia.Styling { @@ -17,9 +18,9 @@ namespace Avalonia.Styling /// A is used to set a value on a /// depending on a condition. /// - public class Setter : ISetter, IAnimationSetter + public class Setter : ISetter, IAnimationSetter, IAvaloniaPropertyVisitor { - private object _value; + private object? _value; /// /// Initializes a new instance of the class. @@ -42,11 +43,7 @@ namespace Avalonia.Styling /// /// Gets or sets the property to set. /// - public AvaloniaProperty Property - { - get; - set; - } + public AvaloniaProperty? Property { get; set; } /// /// Gets or sets the property value. @@ -54,13 +51,9 @@ namespace Avalonia.Styling [Content] [AssignBinding] [DependsOn(nameof(Property))] - public object Value + public object? Value { - get - { - return _value; - } - + get => _value; set { (value as ISetterValue)?.Initialize(this); @@ -68,99 +61,71 @@ namespace Avalonia.Styling } } - /// - /// Applies the setter to a control. - /// - /// The style that is being applied. - /// The control. - /// An optional activator. - public IDisposable Apply(IStyle style, IStyleable control, IObservable activator) + public ISetterInstance Instance(IStyleable target, bool hasActivator) { - Contract.Requires(control != null); + target = target ?? throw new ArgumentNullException(nameof(target)); - if (Property == null) + if (Property is null) { throw new InvalidOperationException("Setter.Property must be set."); } - var value = Value; - var binding = value as IBinding; + var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; - if (binding == null) + if (Value is IBinding binding) { - if (value is ITemplate template) - { - bool isPropertyOfTypeITemplate = typeof(ITemplate).IsAssignableFrom(Property.PropertyType); - - if (!isPropertyOfTypeITemplate) - { - var materialized = template.Build(); - value = materialized; - } - } - - if (activator == null) - { - return control.Bind(Property, ObservableEx.SingleValue(value), BindingPriority.Style); - } - else - { - var description = style?.ToString(); - - var activated = new ActivatedValue(activator, value, description); - return control.Bind(Property, activated, BindingPriority.StyleTrigger); - } + return new PropertySetterBindingInstance(target, Property, priority, binding); } else { - var source = binding.Initiate(control, Property); + var value = Value; - if (source != null) + if (value is ITemplate template && + !typeof(ITemplate).IsAssignableFrom(Property.PropertyType)) { - var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator); - return BindingOperations.Apply(control, Property, cloned, null); + value = template.Build(); } - } - return Disposable.Empty; + var data = new SetterVisitorData + { + target = target, + priority = priority, + value = value, + }; + + Property.Accept(this, ref data); + return data.result!; + } } - private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable activator) + void IAvaloniaPropertyVisitor.Visit( + StyledPropertyBase property, + ref SetterVisitorData data) { - if (activator != null) - { - var description = style?.ToString(); + data.result = new PropertySetterInstance( + data.target, + property, + data.priority, + (T)data.value); + } - switch (mode) - { - case BindingMode.OneTime: - if (sourceInstance.Observable != null) - { - var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); - return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger); - } - else - { - var activated = new ActivatedValue(activator, sourceInstance.Value, description); - return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger); - } - case BindingMode.OneWay: - { - var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); - return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger); - } - default: - { - var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); - return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger); - } - } + void IAvaloniaPropertyVisitor.Visit( + DirectPropertyBase property, + ref SetterVisitorData data) + { + data.result = new PropertySetterInstance( + data.target, + property, + data.priority, + (T)data.value); + } - } - else - { - return sourceInstance.WithPriority(BindingPriority.Style); - } + private struct SetterVisitorData + { + public IStyleable target; + public BindingPriority priority; + public object? value; + public ISetterInstance? result; } } } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 22db7adfe4..c607ee60e5 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -3,12 +3,12 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; -using System.Reactive.Linq; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Metadata; +#nullable enable + namespace Avalonia.Styling { /// @@ -16,15 +16,10 @@ namespace Avalonia.Styling /// public class Style : AvaloniaObject, IStyle, ISetResourceParent { - private static Dictionary _applied = - new Dictionary(); - private IResourceNode _parent; - - private CompositeDisposable _subscriptions; - - private IResourceDictionary _resources; - - private IList _animations; + private IResourceNode? _parent; + private IResourceDictionary? _resources; + private List? _setters; + private List? _animations; /// /// Initializes a new instance of the class. @@ -37,13 +32,13 @@ namespace Avalonia.Styling /// Initializes a new instance of the class. /// /// The style selector. - public Style(Func selector) + public Style(Func selector) { Selector = selector(null); } /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; /// /// Gets or sets a dictionary of style resources. @@ -53,7 +48,7 @@ namespace Avalonia.Styling get => _resources ?? (Resources = new ResourceDictionary()); set { - Contract.Requires(value != null); + value = value ?? throw new ArgumentNullException(nameof(value)); var hadResources = false; @@ -76,117 +71,45 @@ namespace Avalonia.Styling /// /// Gets or sets the style's selector. /// - public Selector Selector { get; set; } + public Selector? Selector { get; set; } /// - /// Gets or sets the style's setters. + /// Gets the style's setters. /// [Content] - public IList Setters { get; set; } = new List(); + public IList Setters => _setters ??= new List(); - public IList Animations - { - get - { - return _animations ?? (_animations = new List()); - } - } - - private CompositeDisposable Subscriptions - { - get - { - return _subscriptions ?? (_subscriptions = new CompositeDisposable(2)); - } - } + /// + /// Gets the style's animations. + /// + public IList Animations => _animations ??= new List(); /// - IResourceNode IResourceNode.ResourceParent => _parent; + IResourceNode? IResourceNode.ResourceParent => _parent; /// bool IResourceProvider.HasResources => _resources?.Count > 0; /// - public bool Attach(IStyleable control, IStyleHost container) + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { - if (Selector != null) - { - var match = Selector.Match(control); - - if (match.IsMatch) - { - var controlSubscriptions = GetSubscriptions(control); - - var animatable = control as Animatable; - - var setters = Setters; - var settersCount = setters.Count; - var animations = Animations; - var animationsCount = animations.Count; - - var subs = new CompositeDisposable(settersCount + (animatable != null ? animationsCount : 0) + 1); - - if (animatable != null) - { - for (var i = 0; i < animationsCount; i++) - { - var animation = animations[i]; - var obsMatch = match.Activator; - - if (match.Result == SelectorMatchResult.AlwaysThisType || - match.Result == SelectorMatchResult.AlwaysThisInstance) - { - obsMatch = Observable.Return(true); - } - - var sub = animation.Apply(animatable, null, obsMatch); - subs.Add(sub); - } - } - - for (var i = 0; i < settersCount; i++) - { - var setter = setters[i]; - var sub = setter.Apply(this, control, match.Activator); - subs.Add(sub); - } + target = target ?? throw new ArgumentNullException(nameof(target)); - subs.Add(Disposable.Create((subs, Subscriptions) , state => state.Subscriptions.Remove(state.subs))); + var match = Selector is object ? Selector.Match(target) : + target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; - controlSubscriptions.Add(subs); - Subscriptions.Add(subs); - } - - return match.Result != SelectorMatchResult.NeverThisType; - } - else if (control == container) + if (match.IsMatch && _setters is object) { - var setters = Setters; - var settersCount = setters.Count; - - var controlSubscriptions = GetSubscriptions(control); - - var subs = new CompositeDisposable(settersCount + 1); - - for (var i = 0; i < settersCount; i++) - { - var setter = setters[i]; - var sub = setter.Apply(this, control, null); - subs.Add(sub); - } - - subs.Add(Disposable.Create((subs, Subscriptions), state => state.Subscriptions.Remove(state.subs))); - - controlSubscriptions.Add(subs); - Subscriptions.Add(subs); - return true; + var instance = new StyleInstance(this, target, _setters, match.Activator); + target.StyleApplied(instance); + instance.Start(); } - return false; + return match.Result; } /// - public bool TryGetResource(object key, out object result) + public bool TryGetResource(object key, out object? result) { result = null; return _resources?.TryGetResource(key, out result) ?? false; @@ -224,44 +147,12 @@ namespace Avalonia.Styling if (parent == null) { - Detach(); + //Detach(); } _parent = parent; } - public void Detach() - { - _subscriptions?.Dispose(); - _subscriptions = null; - } - - private static CompositeDisposable GetSubscriptions(IStyleable control) - { - if (!_applied.TryGetValue(control, out var subscriptions)) - { - subscriptions = new CompositeDisposable(3); - subscriptions.Add(control.StyleDetach.Subscribe(ControlDetach)); - _applied.Add(control, subscriptions); - } - - return subscriptions; - } - - /// - /// Called when a control's is signaled to remove - /// all applied styles. - /// - /// The control. - private static void ControlDetach(IStyleable control) - { - var subscriptions = _applied[control]; - - subscriptions.Dispose(); - - _applied.Remove(control); - } - private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e) { ResourcesChanged?.Invoke(this, e); diff --git a/src/Avalonia.Styling/Styling/StyleActivator.cs b/src/Avalonia.Styling/Styling/StyleActivator.cs deleted file mode 100644 index 63945037d8..0000000000 --- a/src/Avalonia.Styling/Styling/StyleActivator.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; - -namespace Avalonia.Styling -{ - public enum ActivatorMode - { - And, - Or, - } - - public static class StyleActivator - { - public static IObservable And(IList> inputs) - { - if (inputs.Count == 0) - { - throw new ArgumentException("StyleActivator.And inputs may not be empty."); - } - else if (inputs.Count == 1) - { - return inputs[0]; - } - else - { - return inputs.CombineLatest() - .Select(values => values.All(x => x)) - .DistinctUntilChanged(); - } - } - - public static IObservable Or(IList> inputs) - { - if (inputs.Count == 0) - { - throw new ArgumentException("StyleActivator.Or inputs may not be empty."); - } - else if (inputs.Count == 1) - { - return inputs[0]; - } - else - { - return inputs.CombineLatest() - .Select(values => values.Any(x => x)) - .DistinctUntilChanged(); - } - } - } -} diff --git a/src/Avalonia.Styling/Styling/StyleInstance.cs b/src/Avalonia.Styling/Styling/StyleInstance.cs new file mode 100644 index 0000000000..6977f19f59 --- /dev/null +++ b/src/Avalonia.Styling/Styling/StyleInstance.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using Avalonia.Styling.Activators; + +#nullable enable + +namespace Avalonia.Styling +{ + internal class StyleInstance : IStyleInstance, IStyleActivatorSink + { + private readonly List _setters; + private readonly IStyleActivator? _activator; + private bool _active; + + public StyleInstance( + IStyle source, + IStyleable target, + IReadOnlyList setters, + IStyleActivator? activator = null) + { + setters = setters ?? throw new ArgumentNullException(nameof(setters)); + + Source = source ?? throw new ArgumentNullException(nameof(source)); + Target = target ?? throw new ArgumentNullException(nameof(target)); + + _setters = new List(setters.Count); + _activator = activator; + + foreach (var setter in setters) + { + _setters.Add(setter.Instance(target, activator is object)); + } + } + + public IStyle Source { get; } + public IStyleable Target { get; } + + public void Start() + { + if (_activator == null) + { + ActivatorChanged(true); + } + else + { + _activator.Subscribe(this, 0); + } + } + + public void Dispose() + { + ActivatorChanged(false); + _activator?.Dispose(); + } + + private void ActivatorChanged(bool value) + { + if (_active != value) + { + _active = value; + + if (_active) + { + foreach (var setter in _setters) + { + setter.Activate(); + } + } + else + { + foreach (var setter in _setters) + { + setter.Deactivate(); + } + } + } + } + + void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(value); + } +} diff --git a/src/Avalonia.Styling/Styling/Styler.cs b/src/Avalonia.Styling/Styling/Styler.cs index 7ac5c89005..cfd9f65aee 100644 --- a/src/Avalonia.Styling/Styling/Styler.cs +++ b/src/Avalonia.Styling/Styling/Styler.cs @@ -3,35 +3,34 @@ using System; +#nullable enable + namespace Avalonia.Styling { public class Styler : IStyler { - public void ApplyStyles(IStyleable control) + public void ApplyStyles(IStyleable target) { - var styleHost = control as IStyleHost; + target = target ?? throw new ArgumentNullException(nameof(target)); - if (styleHost != null) + if (target is IStyleHost styleHost) { - ApplyStyles(control, styleHost); + ApplyStyles(target, styleHost); } } - private void ApplyStyles(IStyleable control, IStyleHost styleHost) + private void ApplyStyles(IStyleable target, IStyleHost host) { - Contract.Requires(control != null); - Contract.Requires(styleHost != null); - - var parentContainer = styleHost.StylingParent; + var parent = host.StylingParent; - if (parentContainer != null) + if (parent != null) { - ApplyStyles(control, parentContainer); + ApplyStyles(target, parent); } - if (styleHost.IsStylesInitialized) + if (host.IsStylesInitialized) { - styleHost.Styles.Attach(control, styleHost); + host.Styles.TryAttach(target, host); } } } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index fd38c39650..fc579266e8 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -9,6 +9,8 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Controls; +#nullable enable + namespace Avalonia.Styling { /// @@ -16,10 +18,10 @@ namespace Avalonia.Styling /// public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetResourceParent { - private IResourceNode _parent; - private IResourceDictionary _resources; - private AvaloniaList _styles = new AvaloniaList(); - private Dictionary> _cache; + private readonly AvaloniaList _styles = new AvaloniaList(); + private IResourceNode? _parent; + private IResourceDictionary? _resources; + private Dictionary?>? _cache; public Styles() { @@ -60,6 +62,12 @@ namespace Avalonia.Styling () => { }); } + public Styles(IResourceNode parent) + : this() + { + _parent = parent; + } + public event NotifyCollectionChangedEventHandler CollectionChanged { add => _styles.CollectionChanged += value; @@ -67,7 +75,7 @@ namespace Avalonia.Styling } /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; /// public int Count => _styles.Count; @@ -83,7 +91,7 @@ namespace Avalonia.Styling get => _resources ?? (Resources = new ResourceDictionary()); set { - Contract.Requires(value != null); + value = value ?? throw new ArgumentNullException(nameof(Resources)); var hadResources = false; @@ -104,7 +112,7 @@ namespace Avalonia.Styling } /// - IResourceNode IResourceNode.ResourceParent => _parent; + IResourceNode? IResourceNode.ResourceParent => _parent; /// bool ICollection.IsReadOnly => false; @@ -119,66 +127,50 @@ namespace Avalonia.Styling set => _styles[index] = value; } - /// - /// Attaches the style to a control if the style's selector matches. - /// - /// The control to attach to. - /// - /// The control that contains this style. May be null. - /// - public bool Attach(IStyleable control, IStyleHost container) + /// + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { - if (_cache == null) - { - _cache = new Dictionary>(); - } + _cache ??= new Dictionary?>(); - if (_cache.TryGetValue(control.StyleKey, out var cached)) + if (_cache.TryGetValue(target.StyleKey, out var cached)) { - if (cached != null) + if (cached is object) { foreach (var style in cached) { - style.Attach(control, container); + style.TryAttach(target, host); } - return true; + return SelectorMatchResult.AlwaysThisType; + } + else + { + return SelectorMatchResult.NeverThisType; } - - return false; } else { - List result = null; + List? matches = null; - foreach (var style in this) + foreach (var child in this) { - if (style.Attach(control, container)) + if (child.TryAttach(target, host) != SelectorMatchResult.NeverThisType) { - if (result == null) - { - result = new List(); - } - - result.Add(style); + matches ??= new List(); + matches.Add(child); } } - _cache.Add(control.StyleKey, result); - return result != null; - } - } - - public void Detach() - { - foreach (IStyle style in this) - { - style.Detach(); + _cache.Add(target.StyleKey, matches); + + return matches is null ? + SelectorMatchResult.NeverThisType : + SelectorMatchResult.AlwaysThisType; } } /// - public bool TryGetResource(object key, out object value) + public bool TryGetResource(object key, out object? value) { if (_resources != null && _resources.TryGetResource(key, out value)) { diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index 401fa54fb5..71b8828cba 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs @@ -3,11 +3,10 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Reflection; using System.Text; -using Avalonia.Collections; -using Avalonia.Reactive; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -17,13 +16,12 @@ namespace Avalonia.Styling /// internal class TypeNameAndClassSelector : Selector { - private readonly Selector _previous; + private readonly Selector? _previous; private readonly Lazy> _classes = new Lazy>(() => new List()); - private Type _targetType; - - private string _selectorString; + private Type? _targetType; + private string? _selectorString; - public static TypeNameAndClassSelector OfType(Selector previous, Type targetType) + public static TypeNameAndClassSelector OfType(Selector? previous, Type targetType) { var result = new TypeNameAndClassSelector(previous); result._targetType = targetType; @@ -32,7 +30,7 @@ namespace Avalonia.Styling return result; } - public static TypeNameAndClassSelector Is(Selector previous, Type targetType) + public static TypeNameAndClassSelector Is(Selector? previous, Type targetType) { var result = new TypeNameAndClassSelector(previous); result._targetType = targetType; @@ -41,7 +39,7 @@ namespace Avalonia.Styling return result; } - public static TypeNameAndClassSelector ForName(Selector previous, string name) + public static TypeNameAndClassSelector ForName(Selector? previous, string name) { var result = new TypeNameAndClassSelector(previous); result.Name = name; @@ -49,7 +47,7 @@ namespace Avalonia.Styling return result; } - public static TypeNameAndClassSelector ForClass(Selector previous, string className) + public static TypeNameAndClassSelector ForClass(Selector? previous, string className) { var result = new TypeNameAndClassSelector(previous); result.Classes.Add(className); @@ -57,7 +55,7 @@ namespace Avalonia.Styling return result; } - protected TypeNameAndClassSelector(Selector previous) + protected TypeNameAndClassSelector(Selector? previous) { _previous = previous; } @@ -68,10 +66,10 @@ namespace Avalonia.Styling /// /// Gets the name of the control to match. /// - public string Name { get; set; } + public string? Name { get; set; } /// - public override Type TargetType => _targetType ?? _previous?.TargetType; + public override Type? TargetType => _targetType ?? _previous?.TargetType; /// public override bool IsCombinator => false; @@ -130,12 +128,12 @@ namespace Avalonia.Styling { if (subscribe) { - var observable = new ClassObserver(control.Classes, _classes.Value); + var observable = new StyleClassActivator(control.Classes, _classes.Value); return new SelectorMatch(observable); } - if (!AreClassesMatching(control.Classes, Classes)) + if (!StyleClassActivator.AreClassesMatching(control.Classes, Classes)) { return SelectorMatch.NeverThisInstance; } @@ -144,7 +142,7 @@ namespace Avalonia.Styling return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; } - protected override Selector MovePrevious() => _previous; + protected override Selector? MovePrevious() => _previous; private string BuildSelectorString() { @@ -190,80 +188,5 @@ namespace Avalonia.Styling return builder.ToString(); } - - private static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) - { - int remainingMatches = toMatch.Count; - int classesCount = classes.Count; - - // Early bail out - we can't match if control does not have enough classes. - if (classesCount < remainingMatches) - { - return false; - } - - for (var i = 0; i < classesCount; i++) - { - var c = classes[i]; - - if (toMatch.Contains(c)) - { - --remainingMatches; - - // Already matched so we can skip checking other classes. - if (remainingMatches == 0) - { - break; - } - } - } - - return remainingMatches == 0; - } - - private sealed class ClassObserver : LightweightObservableBase - { - private readonly IList _match; - private readonly IAvaloniaReadOnlyList _classes; - private bool _hasMatch; - - public ClassObserver(IAvaloniaReadOnlyList classes, IList match) - { - _classes = classes; - _match = match; - } - - protected override void Deinitialize() => _classes.CollectionChanged -= ClassesChanged; - - protected override void Initialize() - { - _hasMatch = IsMatching(); - _classes.CollectionChanged += ClassesChanged; - } - - protected override void Subscribed(IObserver observer, bool first) - { - observer.OnNext(_hasMatch); - } - - private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (e.Action != NotifyCollectionChangedAction.Move) - { - var hasMatch = IsMatching(); - - if (hasMatch != _hasMatch) - { - PublishNext(hasMatch); - _hasMatch = hasMatch; - } - } - } - - private bool IsMatching() - { - return AreClassesMatching(_classes, _match); - } - } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 41eab79ed8..e5e28b344f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -4,6 +4,7 @@ using Avalonia.Styling; using System; using Avalonia.Controls; +using System.Collections.Generic; namespace Avalonia.Markup.Xaml.Styling { @@ -67,23 +68,7 @@ namespace Avalonia.Markup.Xaml.Styling IResourceNode IResourceNode.ResourceParent => _parent; /// - public bool Attach(IStyleable control, IStyleHost container) - { - if (Source != null) - { - return Loaded.Attach(control, container); - } - - return false; - } - - public void Detach() - { - if (Source != null) - { - Loaded.Detach(); - } - } + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost host) => Loaded.TryAttach(target, host); /// public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value); diff --git a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs index d24a646f74..b9d0b53728 100644 --- a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs +++ b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs @@ -45,7 +45,7 @@ namespace Avalonia.Benchmarks.Styling { _window.Styles.Add(new Style(x => x.OfType().Class("foo").Class("bar").Class("baz")) { - Setters = new[] + Setters = { new Setter(TextBox.TextProperty, "foo"), } diff --git a/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs b/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs index 7bccd65c81..7dad517e51 100644 --- a/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.CompilerServices; using Avalonia.Controls; +using Avalonia.Styling; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; @@ -34,9 +35,8 @@ namespace Avalonia.Benchmarks.Styling { var styles = UnitTestApplication.Current.Styles; - styles.Attach(_control, UnitTestApplication.Current); - - styles.Detach(); + styles.TryAttach(_control, UnitTestApplication.Current); + ((IStyleable)_control).DetachStyles(); } public void Dispose() diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index d36d0b609b..53b5b87ea2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -363,7 +363,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter( TemplatedControl.TemplateProperty, @@ -399,7 +399,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter( TemplatedControl.TemplateProperty, @@ -438,7 +438,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter( TemplatedControl.TemplateProperty, @@ -458,7 +458,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter( TemplatedControl.TemplateProperty, diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index a9e86d71ee..6101e7b3d4 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -163,7 +163,7 @@ namespace Avalonia.Controls.UnitTests { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter(TemplatedControl.TemplateProperty, template) } diff --git a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs index 9d3e568582..fddef4ec88 100644 --- a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs @@ -25,7 +25,7 @@ namespace Avalonia.Controls.UnitTests { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter(TemplatedControl.TemplateProperty, GetTemplate()) } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index d82300b964..ec9a6ba77f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -139,7 +139,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters get { throw new NotImplementedException(); } } - IObservable IStyleable.StyleDetach { get; } + public void DetachStyles() + { + throw new NotImplementedException(); + } + + public void StyleApplied(IStyleInstance instance) + { + throw new NotImplementedException(); + } } private class AttachedOwner diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs index 2dc6c4a7fb..44a40af93d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs @@ -53,42 +53,7 @@ namespace Avalonia.Markup.Xaml.UnitTests } }; - setter.Apply(null, control, null); - Assert.Equal("foo", control.Text); - - control.Text = "bar"; - Assert.Equal("bar", data.Foo); - } - } - - [Fact] - public void Setter_With_TwoWay_Binding_And_Activator_Should_Update_Source() - { - using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) - { - var data = new Data - { - Foo = "foo", - }; - - var control = new TextBox - { - DataContext = data, - }; - - var setter = new Setter - { - Property = TextBox.TextProperty, - Value = new Binding - { - Path = "Foo", - Mode = BindingMode.TwoWay - } - }; - - var activator = Observable.Never().StartWith(true); - - setter.Apply(null, control, activator); + setter.Instance(control, false).Activate(); Assert.Equal("foo", control.Text); control.Text = "bar"; diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs deleted file mode 100644 index 7773d4767a..0000000000 --- a/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Xunit; - -namespace Avalonia.Styling.UnitTests -{ - public class ActivatedObservableTests - { - [Fact] - public void Should_Produce_Correct_Values() - { - var activator = new BehaviorSubject(false); - var source = new BehaviorSubject(1); - var target = new ActivatedObservable(activator, source, string.Empty); - var result = new List(); - - target.Subscribe(x => result.Add(x)); - - activator.OnNext(true); - source.OnNext(2); - activator.OnNext(false); - source.OnNext(3); - activator.OnNext(true); - - Assert.Equal( - new[] - { - AvaloniaProperty.UnsetValue, - 1, - 2, - AvaloniaProperty.UnsetValue, - 3, - }, - result); - } - - [Fact] - public void Should_Complete_When_Source_Completes() - { - var activator = new BehaviorSubject(false); - var source = new BehaviorSubject(1); - var target = new ActivatedObservable(activator, source, string.Empty); - var completed = false; - - target.Subscribe(_ => { }, () => completed = true); - source.OnCompleted(); - - Assert.True(completed); - } - - [Fact] - public void Should_Error_When_Source_Errors() - { - var activator = new BehaviorSubject(false); - var source = new BehaviorSubject(1); - var target = new ActivatedObservable(activator, source, string.Empty); - var error = new Exception(); - var completed = false; - - target.Subscribe(_ => { }, x => completed = true); - source.OnError(error); - - Assert.True(completed); - } - } -} diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs deleted file mode 100644 index 03f91d97a1..0000000000 --- a/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive.Disposables; -using System.Reactive.Subjects; -using Xunit; - -namespace Avalonia.Styling.UnitTests -{ - public class ActivatedSubjectTests - { - [Fact] - public void Should_Set_Values() - { - var activator = new BehaviorSubject(false); - var source = new TestSubject(); - var target = new ActivatedSubject(activator, source, string.Empty); - - target.Subscribe(); - target.OnNext("bar"); - Assert.Equal(AvaloniaProperty.UnsetValue, source.Value); - activator.OnNext(true); - target.OnNext("baz"); - Assert.Equal("baz", source.Value); - activator.OnNext(false); - Assert.Equal(AvaloniaProperty.UnsetValue, source.Value); - target.OnNext("bax"); - activator.OnNext(true); - Assert.Equal("bax", source.Value); - } - - [Fact] - public void Should_Invoke_OnCompleted_On_Activator_Completed() - { - var activator = new BehaviorSubject(false); - var source = new TestSubject(); - var target = new ActivatedSubject(activator, source, string.Empty); - - target.Subscribe(); - activator.OnCompleted(); - - Assert.True(source.Completed); - } - - [Fact] - public void Should_Invoke_OnError_On_Activator_Error() - { - var activator = new BehaviorSubject(false); - var source = new TestSubject(); - var target = new ActivatedSubject(activator, source, string.Empty); - var targetError = default(Exception); - var error = new Exception(); - - target.Subscribe(_ => { }, e => targetError = e); - activator.OnError(error); - - Assert.Same(error, source.Error); - Assert.Same(error, targetError); - } - - private class TestSubject : ISubject - { - private IObserver _observer; - - public bool Completed { get; set; } - public Exception Error { get; set; } - public object Value { get; set; } = AvaloniaProperty.UnsetValue; - - public void OnCompleted() - { - Completed = true; - } - - public void OnError(Exception error) - { - Error = error; - } - - public void OnNext(object value) - { - Value = value; - } - - public IDisposable Subscribe(IObserver observer) - { - _observer = observer; - return Disposable.Empty; - } - } - } -} diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs deleted file mode 100644 index 92a7c1bd1f..0000000000 --- a/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Microsoft.Reactive.Testing; -using Xunit; - -namespace Avalonia.Styling.UnitTests -{ - public class ActivatedValueTests - { - [Fact] - public void Should_Produce_Correct_Values() - { - var activator = new BehaviorSubject(false); - var target = new ActivatedValue(activator, 1, string.Empty); - var result = new List(); - - target.Subscribe(x => result.Add(x)); - - activator.OnNext(true); - activator.OnNext(false); - - Assert.Equal(new[] { AvaloniaProperty.UnsetValue, 1, AvaloniaProperty.UnsetValue }, result); - } - - [Fact] - public void Should_Complete_When_Activator_Completes() - { - var activator = new BehaviorSubject(false); - var target = new ActivatedValue(activator, 1, string.Empty); - var completed = false; - - target.Subscribe(_ => { }, () => completed = true); - activator.OnCompleted(); - - Assert.True(completed); - } - - [Fact] - public void Should_Error_When_Activator_Errors() - { - var activator = new BehaviorSubject(false); - var target = new ActivatedValue(activator, 1, string.Empty); - var error = new Exception(); - var completed = false; - - target.Subscribe(_ => { }, x => completed = true); - activator.OnError(error); - - Assert.True(completed); - } - - [Fact] - public void Should_Unsubscribe_From_Activator_When_All_Subscriptions_Disposed() - { - var scheduler = new TestScheduler(); - var activator1 = scheduler.CreateColdObservable(); - var activator2 = scheduler.CreateColdObservable(); - var activator = StyleActivator.And(new[] { activator1, activator2 }); - var target = new ActivatedValue(activator, 1, string.Empty); - - var subscription = target.Subscribe(_ => { }); - Assert.Equal(1, activator1.Subscriptions.Count); - Assert.Equal(Subscription.Infinite, activator1.Subscriptions[0].Unsubscribe); - - subscription.Dispose(); - Assert.Equal(1, activator1.Subscriptions.Count); - Assert.Equal(0, activator1.Subscriptions[0].Unsubscribe); - } - } -} diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs index fd25b17ba4..00b90f1239 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs @@ -36,7 +36,7 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1 { - Classes = new Classes { "foo" }, + Classes = { "foo" }, }; var target = default(Selector).Class("foo"); @@ -51,7 +51,7 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1 { - Classes = new Classes { "bar" }, + Classes = { "bar" }, }; var target = default(Selector).Class("foo"); @@ -66,7 +66,7 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1 { - Classes = new Classes { "foo" }, + Classes = { "foo" }, TemplatedParent = new Mock().Object, }; @@ -83,7 +83,7 @@ namespace Avalonia.Styling.UnitTests var control = new Control1(); var target = default(Selector).Class("foo"); - var activator = target.Match(control).Activator; + var activator = target.Match(control).Activator.ToObservable(); Assert.False(await activator.Take(1)); control.Classes.Add("foo"); @@ -95,11 +95,11 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1 { - Classes = new Classes { "foo" }, + Classes = { "foo" }, }; var target = default(Selector).Class("foo"); - var activator = target.Match(control).Activator; + var activator = target.Match(control).Activator.ToObservable(); Assert.True(await activator.Take(1)); control.Classes.Remove("foo"); @@ -111,7 +111,7 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1(); var target = default(Selector).Class("foo").Class("bar"); - var activator = target.Match(control).Activator; + var activator = target.Match(control).Activator.ToObservable(); Assert.False(await activator.Take(1)); control.Classes.Add("foo"); @@ -128,7 +128,7 @@ namespace Avalonia.Styling.UnitTests // Test for #1698 var control = new Control1 { - Classes = new Classes { "foo" }, + Classes = { "foo" }, }; var target = default(Selector).Class("foo"); diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index 099562b1cf..1128120824 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -90,7 +90,7 @@ namespace Avalonia.Styling.UnitTests child.LogicalParent = parent; var selector = default(Selector).OfType().Class("foo").Descendant().OfType(); - var activator = selector.Match(child).Activator; + var activator = selector.Match(child).Activator.ToObservable(); Assert.False(await activator.Take(1)); parent.Classes.Add("foo"); diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs index e8be44ed3b..a1ced14108 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs @@ -85,6 +85,35 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(SelectorMatchResult.NeverThisType, match.Result); } + [Fact] + public void Control_With_Class_Descendent_Of_Control_With_Two_Classes() + { + var textBlock = new TextBlock(); + var control = new Button { Content = textBlock }; + + control.ApplyTemplate(); + + var selector = default(Selector) + .OfType