Browse Source

Refactored style activators.

- Always evaluate the active state from current information, don't rely on subscriptions to fire as the current state may not be up-to-date
- Don't notify the `IStyleActivatorSink` of a change immediately on subscription
refactor/style-priorities
Steven Kirk 4 years ago
parent
commit
09d0c3ae3e
  1. 55
      src/Avalonia.Base/Styling/Activators/AndActivator.cs
  2. 14
      src/Avalonia.Base/Styling/Activators/IStyleActivator.cs
  3. 8
      src/Avalonia.Base/Styling/Activators/NotActivator.cs
  4. 19
      src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
  5. 49
      src/Avalonia.Base/Styling/Activators/OrActivator.cs
  6. 13
      src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs
  7. 54
      src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs
  8. 25
      src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs
  9. 16
      src/Avalonia.Base/Styling/StyleInstance.cs
  10. 8
      tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs
  11. 6
      tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs

55
src/Avalonia.Base/Styling/Activators/AndActivator.cs

@ -1,6 +1,4 @@
#nullable enable using System.Collections.Generic;
using System.Collections.Generic;
namespace Avalonia.Styling.Activators namespace Avalonia.Styling.Activators
{ {
@ -11,49 +9,35 @@ namespace Avalonia.Styling.Activators
internal class AndActivator : StyleActivatorBase, IStyleActivatorSink internal class AndActivator : StyleActivatorBase, IStyleActivatorSink
{ {
private List<IStyleActivator>? _sources; private List<IStyleActivator>? _sources;
private ulong _flags;
private ulong _mask;
public int Count => _sources?.Count ?? 0; public int Count => _sources?.Count ?? 0;
public override bool IsActive
{
get
{
if (_sources is null)
return false;
foreach (var source in _sources)
{
if (!source.IsActive)
return false;
}
return true;
}
}
public void Add(IStyleActivator activator) public void Add(IStyleActivator activator)
{ {
if (IsSubscribed)
throw new AvaloniaInternalException("AndActivator is already subscribed.");
_sources ??= new List<IStyleActivator>(); _sources ??= new List<IStyleActivator>();
_sources.Add(activator); _sources.Add(activator);
} }
void IStyleActivatorSink.OnNext(bool value, int tag) void IStyleActivatorSink.OnNext(bool value, int tag) => ReevaluateIsActive();
protected override bool EvaluateIsActive()
{ {
if (value) if (_sources is null || _sources.Count == 0)
{ return true;
_flags |= 1ul << tag;
} var count = _sources.Count;
else var mask = (1ul << count) - 1;
{ var flags = 0UL;
_flags &= ~(1ul << tag);
}
if (_mask != 0) for (var i = 0; i < count; ++i)
{ {
PublishNext(_flags == _mask); if (_sources[i].IsActive)
flags |= 1ul << i;
} }
return flags == mask;
} }
protected override void Initialize() protected override void Initialize()
@ -66,9 +50,6 @@ namespace Avalonia.Styling.Activators
{ {
source.Subscribe(this, i++); source.Subscribe(this, i++);
} }
_mask = (1ul << Count) - 1;
PublishNext(_flags == _mask);
} }
} }
@ -81,8 +62,6 @@ namespace Avalonia.Styling.Activators
source.Unsubscribe(this); source.Unsubscribe(this);
} }
} }
_mask = 0;
} }
} }
} }

14
src/Avalonia.Base/Styling/Activators/IStyleActivator.cs

@ -21,13 +21,27 @@ namespace Avalonia.Styling.Activators
/// <summary> /// <summary>
/// Gets a value indicating whether the style is activated. /// Gets a value indicating whether the style is activated.
/// </summary> /// </summary>
/// <remarks>
/// This property should read directly from its inputs and not rely on any subscriptions
/// to fire in order to be up-to-date. If a change in active state occurs when reading
/// this property then any subscribed <see cref="IStyleActivatorSink"/> should not be
/// notified of the change.
/// </remarks>
bool IsActive { get; } bool IsActive { get; }
/// <summary>
/// Gets a value indicating whether the style is subscribed.
/// </summary>
bool IsSubscribed { get; }
/// <summary> /// <summary>
/// Subscribes to the activator. /// Subscribes to the activator.
/// </summary> /// </summary>
/// <param name="sink">The listener.</param> /// <param name="sink">The listener.</param>
/// <param name="tag">An optional tag.</param> /// <param name="tag">An optional tag.</param>
/// <remarks>
/// This method should not call <see cref="IStyleActivatorSink.OnNext(bool, int)"/>.
/// </remarks>
void Subscribe(IStyleActivatorSink sink, int tag = 0); void Subscribe(IStyleActivatorSink sink, int tag = 0);
/// <summary> /// <summary>

8
src/Avalonia.Base/Styling/Activators/NotActivator.cs

@ -1,6 +1,4 @@
#nullable enable namespace Avalonia.Styling.Activators
namespace Avalonia.Styling.Activators
{ {
/// <summary> /// <summary>
/// An <see cref="IStyleActivator"/> which inverts the state of an input activator. /// An <see cref="IStyleActivator"/> which inverts the state of an input activator.
@ -9,8 +7,8 @@ namespace Avalonia.Styling.Activators
{ {
private readonly IStyleActivator _source; private readonly IStyleActivator _source;
public NotActivator(IStyleActivator source) => _source = source; public NotActivator(IStyleActivator source) => _source = source;
public override bool IsActive => !_source.IsActive; void IStyleActivatorSink.OnNext(bool value, int tag) => ReevaluateIsActive();
void IStyleActivatorSink.OnNext(bool value, int tag) => PublishNext(!value); protected override bool EvaluateIsActive() => !_source.IsActive;
protected override void Initialize() => _source.Subscribe(this, 0); protected override void Initialize() => _source.Subscribe(this, 0);
protected override void Deinitialize() => _source.Unsubscribe(this); protected override void Deinitialize() => _source.Unsubscribe(this);
} }

19
src/Avalonia.Base/Styling/Activators/NthChildActivator.cs

@ -26,30 +26,25 @@ namespace Avalonia.Styling.Activators
_reversed = reversed; _reversed = reversed;
} }
public override bool IsActive => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch; protected override bool EvaluateIsActive()
protected override void Initialize()
{ {
PublishNext(IsActive); return NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
_provider.ChildIndexChanged += ChildIndexChanged;
} }
protected override void Deinitialize() protected override void Initialize() => _provider.ChildIndexChanged += ChildIndexChanged;
{ protected override void Deinitialize() => _provider.ChildIndexChanged -= ChildIndexChanged;
_provider.ChildIndexChanged -= ChildIndexChanged;
}
private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e) private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)
{ {
// Run matching again if: // Run matching again if:
// 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index. // 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index.
// 2. e.Child is null, when all children indeces were changed. // 2. e.Child is null, when all children indices were changed.
// 3. Subscribed child index was changed. // 3. Subscribed child index was changed.
if (_reversed if (_reversed
|| e.Child is null || e.Child is null
|| e.Child == _control) || e.Child == _control)
{ {
PublishNext(IsActive); ReevaluateIsActive();
} }
} }
} }

49
src/Avalonia.Base/Styling/Activators/OrActivator.cs

@ -1,6 +1,4 @@
#nullable enable using System.Collections.Generic;
using System.Collections.Generic;
namespace Avalonia.Styling.Activators namespace Avalonia.Styling.Activators
{ {
@ -11,49 +9,29 @@ namespace Avalonia.Styling.Activators
internal class OrActivator : StyleActivatorBase, IStyleActivatorSink internal class OrActivator : StyleActivatorBase, IStyleActivatorSink
{ {
private List<IStyleActivator>? _sources; private List<IStyleActivator>? _sources;
private ulong _flags;
private bool _initializing;
public int Count => _sources?.Count ?? 0; public int Count => _sources?.Count ?? 0;
public override bool IsActive
{
get
{
if (_sources is null)
return false;
foreach (var source in _sources)
{
if (source.IsActive)
return true;
}
return false;
}
}
public void Add(IStyleActivator activator) public void Add(IStyleActivator activator)
{ {
_sources ??= new List<IStyleActivator>(); _sources ??= new List<IStyleActivator>();
_sources.Add(activator); _sources.Add(activator);
} }
void IStyleActivatorSink.OnNext(bool value, int tag) void IStyleActivatorSink.OnNext(bool value, int tag) => ReevaluateIsActive();
protected override bool EvaluateIsActive()
{ {
if (value) if (_sources is null || _sources.Count == 0)
{ return true;
_flags |= 1ul << tag;
}
else
{
_flags &= ~(1ul << tag);
}
if (!_initializing) foreach (var source in _sources)
{ {
PublishNext(_flags != 0); if (source.IsActive)
return true;
} }
return false;
} }
protected override void Initialize() protected override void Initialize()
@ -62,15 +40,10 @@ namespace Avalonia.Styling.Activators
{ {
var i = 0; var i = 0;
_initializing = true;
foreach (var source in _sources) foreach (var source in _sources)
{ {
source.Subscribe(this, i++); source.Subscribe(this, i++);
} }
_initializing = false;
PublishNext(_flags != 0);
} }
} }

13
src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs

@ -1,7 +1,5 @@
using System; using System;
#nullable enable
namespace Avalonia.Styling.Activators namespace Avalonia.Styling.Activators
{ {
/// <summary> /// <summary>
@ -24,13 +22,10 @@ namespace Avalonia.Styling.Activators
_value = value; _value = value;
} }
public override bool IsActive protected override bool EvaluateIsActive()
{ {
get var value = _control.GetValue(_property);
{ return PropertyEqualsSelector.Compare(_property.PropertyType, value, _value);
var value = _control.GetValue(_property);
return PropertyEqualsSelector.Compare(_property.PropertyType, value, _value);
}
} }
protected override void Initialize() protected override void Initialize()
@ -42,6 +37,6 @@ namespace Avalonia.Styling.Activators
void IObserver<object?>.OnCompleted() { } void IObserver<object?>.OnCompleted() { }
void IObserver<object?>.OnError(Exception error) { } void IObserver<object?>.OnError(Exception error) { }
void IObserver<object?>.OnNext(object? value) => PublishNext(IsActive); void IObserver<object?>.OnNext(object? value) => ReevaluateIsActive();
} }
} }

54
src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs

@ -7,22 +7,23 @@ namespace Avalonia.Styling.Activators
{ {
private IStyleActivatorSink? _sink; private IStyleActivatorSink? _sink;
private int _tag; private int _tag;
private bool? _value; private bool _value;
public abstract bool IsActive { get; } public bool IsActive => _value = EvaluateIsActive();
public bool IsSubscribed => _sink is not null;
public void Subscribe(IStyleActivatorSink sink, int tag = 0) public void Subscribe(IStyleActivatorSink sink, int tag = 0)
{ {
if (_sink is null) if (_sink is null)
{ {
Initialize();
_sink = sink; _sink = sink;
_tag = tag; _tag = tag;
_value = null;
Initialize();
} }
else else
{ {
throw new AvaloniaInternalException("Cannot subscribe to a StyleActivator more than once."); throw new AvaloniaInternalException("StyleActivator is already subscribed.");
} }
} }
@ -37,22 +38,51 @@ namespace Avalonia.Styling.Activators
Deinitialize(); Deinitialize();
} }
public void PublishNext(bool value) public void Dispose()
{ {
if (_value != value) _sink = null;
Deinitialize();
}
/// <summary>
/// Evaluates the <see cref="IsActive"/> value.
/// </summary>
/// <remarks>
/// This method should read directly from its inputs and not rely on any subscriptions to
/// fire in order to be up-to-date.
/// </remarks>
protected abstract bool EvaluateIsActive();
/// <summary>
/// Called from a derived class when the <see cref="IsActive"/> state should be re-evaluated
/// and the subscriber notified of any change.
/// </summary>
/// <returns>
/// The evaluated active state;
/// </returns>
protected bool ReevaluateIsActive()
{
var value = EvaluateIsActive();
if (value != _value)
{ {
_value = value; _value = value;
_sink?.OnNext(value, _tag); _sink?.OnNext(value, _tag);
} }
}
public void Dispose() return value;
{
_sink = null;
Deinitialize();
} }
/// <summary>
/// Called in response to a <see cref="Subscribe(IStyleActivatorSink, int)"/> to allow the
/// derived class to set up any necessary subscriptions.
/// </summary>
protected abstract void Initialize(); protected abstract void Initialize();
/// <summary>
/// Called in response to an <see cref="Unsubscribe(IStyleActivatorSink)"/> or
/// <see cref="Dispose"/> to allow the derived class to dispose any active subscriptions.
/// </summary>
protected abstract void Deinitialize(); protected abstract void Deinitialize();
} }
} }

25
src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs

@ -1,10 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
#nullable enable
namespace Avalonia.Styling.Activators namespace Avalonia.Styling.Activators
{ {
/// <summary> /// <summary>
@ -22,8 +18,6 @@ namespace Avalonia.Styling.Activators
_match = match; _match = match;
} }
public override bool IsActive => AreClassesMatching(_classes, _match);
public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch) public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch)
{ {
int remainingMatches = toMatch.Count; int remainingMatches = toMatch.Count;
@ -54,20 +48,9 @@ namespace Avalonia.Styling.Activators
return remainingMatches == 0; return remainingMatches == 0;
} }
void IClassesChangedListener.Changed() void IClassesChangedListener.Changed() => ReevaluateIsActive();
{ protected override bool EvaluateIsActive() => AreClassesMatching(_classes, _match);
PublishNext(IsActive); protected override void Initialize() => _classes.AddListener(this);
} protected override void Deinitialize() => _classes.RemoveListener(this);
protected override void Initialize()
{
PublishNext(IsActive);
_classes.AddListener(this);
}
protected override void Deinitialize()
{
_classes.RemoveListener(this);
}
} }
} }

16
src/Avalonia.Base/Styling/StyleInstance.cs

@ -20,8 +20,6 @@ namespace Avalonia.Styling
{ {
private readonly IStyleActivator? _activator; private readonly IStyleActivator? _activator;
private List<ISetterInstance>? _setters; private List<ISetterInstance>? _setters;
private bool _isActivatorInitializing;
private bool _isActivatorSubscribed;
public StyleInstance(IStyle style, IStyleActivator? activator) public StyleInstance(IStyle style, IStyleActivator? activator)
{ {
@ -36,14 +34,8 @@ namespace Avalonia.Styling
{ {
get get
{ {
if (_activator is object && !_isActivatorSubscribed) if (_activator?.IsSubscribed == false)
{
_isActivatorInitializing = true;
_activator.Subscribe(this); _activator.Subscribe(this);
_isActivatorInitializing = false;
_isActivatorSubscribed = true;
}
return _activator?.IsActive ?? true; return _activator?.IsActive ?? true;
} }
} }
@ -65,10 +57,6 @@ namespace Avalonia.Styling
_activator?.Dispose(); _activator?.Dispose();
} }
void IStyleActivatorSink.OnNext(bool value, int tag) void IStyleActivatorSink.OnNext(bool value, int tag) => Owner?.OnFrameActivationChanged(this);
{
if (!_isActivatorInitializing)
Owner?.OnFrameActivationChanged(this);
}
} }
} }

8
tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs

@ -292,7 +292,13 @@ namespace Avalonia.Base.UnitTests.Styling
private class ActivatorSink : IStyleActivatorSink private class ActivatorSink : IStyleActivatorSink
{ {
public ActivatorSink(IStyleActivator source) => source.Subscribe(this); public ActivatorSink(IStyleActivator source)
{
source.Subscribe(this);
Active = source.IsActive;
}
public bool Active { get; private set; } public bool Active { get; private set; }
public void OnNext(bool value, int tag) => Active = value; public void OnNext(bool value, int tag) => Active = value;
} }

6
tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs

@ -33,8 +33,14 @@ namespace Avalonia.Base.UnitTests.Styling
private readonly IStyleActivator _source; private readonly IStyleActivator _source;
public ObservableAdapter(IStyleActivator source) => _source = source; public ObservableAdapter(IStyleActivator source) => _source = source;
protected override void Initialize() => _source.Subscribe(this); protected override void Initialize() => _source.Subscribe(this);
protected override void Deinitialize() => _source.Unsubscribe(this); protected override void Deinitialize() => _source.Unsubscribe(this);
protected override void Subscribed(IObserver<bool> observer, bool first)
{
observer.OnNext(_source.IsActive);
}
void IStyleActivatorSink.OnNext(bool value, int tag) void IStyleActivatorSink.OnNext(bool value, int tag)
{ {

Loading…
Cancel
Save