Browse Source
- Don't use Rx in the styling system. Instead introduces `IStyleActivator` which is like an `IObservable<bool>`-lite in order to cut down on allocations. - #nullable enable on touched filespull/3636/head
56 changed files with 1166 additions and 1486 deletions
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// An observable which is switched on or off according to an activator observable.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// An <see cref="ActivatedObservable"/> has two inputs: an activator observable and a
|
|||
/// <see cref="Source"/> observable which produces the activated value. When the activator
|
|||
/// produces true, the <see cref="ActivatedObservable"/> will produce the current activated
|
|||
/// value. When the activator produces false it will produce
|
|||
/// <see cref="AvaloniaProperty.UnsetValue"/>.
|
|||
/// </remarks>
|
|||
internal class ActivatedObservable : ActivatedValue, IDescription |
|||
{ |
|||
private IDisposable _sourceSubscription; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
|
|||
/// </summary>
|
|||
/// <param name="activator">The activator.</param>
|
|||
/// <param name="source">An observable that produces the activated value.</param>
|
|||
/// <param name="description">The binding description.</param>
|
|||
public ActivatedObservable( |
|||
IObservable<bool> activator, |
|||
IObservable<object> source, |
|||
string description) |
|||
: base(activator, AvaloniaProperty.UnsetValue, description) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(source != null); |
|||
|
|||
Source = source; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets an observable which produces the <see cref="ActivatedValue"/>.
|
|||
/// </summary>
|
|||
public IObservable<object> 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<object> |
|||
{ |
|||
public ValueListener(ActivatedObservable parent) |
|||
: base(parent) |
|||
{ |
|||
} |
|||
protected new ActivatedObservable Parent => (ActivatedObservable)base.Parent; |
|||
|
|||
void IObserver<object>.OnCompleted() => Parent.CompletedReceived(); |
|||
void IObserver<object>.OnError(Exception error) => Parent.ErrorReceived(error); |
|||
void IObserver<object>.OnNext(object value) => Parent.NotifyValue(value); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// A subject which is switched on or off according to an activator observable.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// An <see cref="ActivatedSubject"/> extends <see cref="ActivatedObservable"/> to
|
|||
/// be an <see cref="ISubject{Object}"/>. When the object is active then values
|
|||
/// received via <see cref="OnNext(object)"/> will be passed to the source subject.
|
|||
/// </remarks>
|
|||
internal class ActivatedSubject : ActivatedObservable, ISubject<object>, IDescription |
|||
{ |
|||
private bool _completed; |
|||
private object _pushValue; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ActivatedSubject"/> class.
|
|||
/// </summary>
|
|||
/// <param name="activator">The activator.</param>
|
|||
/// <param name="source">An observable that produces the activated value.</param>
|
|||
/// <param name="description">The binding description.</param>
|
|||
public ActivatedSubject( |
|||
IObservable<bool> activator, |
|||
ISubject<object> source, |
|||
string description) |
|||
: base(activator, source, description) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the underlying subject.
|
|||
/// </summary>
|
|||
public new ISubject<object> Source |
|||
{ |
|||
get { return (ISubject<object>)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); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// An value which is switched on or off according to an activator observable.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// An <see cref="ActivatedValue"/> has two inputs: an activator observable and an
|
|||
/// <see cref="Value"/>. When the activator produces true, the
|
|||
/// <see cref="ActivatedValue"/> will produce the current value. When the activator
|
|||
/// produces false it will produce <see cref="AvaloniaProperty.UnsetValue"/>.
|
|||
/// </remarks>
|
|||
internal class ActivatedValue : LightweightObservableBase<object>, IDescription |
|||
{ |
|||
private static readonly object NotSent = new object(); |
|||
private IDisposable _activatorSubscription; |
|||
private object _value; |
|||
private object _last = NotSent; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
|
|||
/// </summary>
|
|||
/// <param name="activator">The activator.</param>
|
|||
/// <param name="value">The activated value.</param>
|
|||
/// <param name="description">The binding description.</param>
|
|||
public ActivatedValue( |
|||
IObservable<bool> activator, |
|||
object value, |
|||
string description) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(activator != null); |
|||
|
|||
Activator = activator; |
|||
Value = value; |
|||
Description = description; |
|||
Listener = CreateListener(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the activator observable.
|
|||
/// </summary>
|
|||
public IObservable<bool> Activator { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a description of the binding.
|
|||
/// </summary>
|
|||
public string Description { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the activator is active.
|
|||
/// </summary>
|
|||
public bool? IsActive { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the value that will be produced when <see cref="IsActive"/> is true.
|
|||
/// </summary>
|
|||
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<object> 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<bool> |
|||
{ |
|||
public ActivatorListener(ActivatedValue parent) |
|||
{ |
|||
Parent = parent; |
|||
} |
|||
|
|||
protected ActivatedValue Parent { get; } |
|||
|
|||
void IObserver<bool>.OnCompleted() => Parent.CompletedReceived(); |
|||
void IObserver<bool>.OnError(Exception error) => Parent.ErrorReceived(error); |
|||
void IObserver<bool>.OnNext(bool value) => Parent.ActiveChanged(value); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
#nullable enable |
|||
|
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Styling.Activators |
|||
{ |
|||
internal class AndActivator : StyleActivatorBase, IStyleActivatorSink |
|||
{ |
|||
private List<IStyleActivator>? _sources; |
|||
private ulong _flags; |
|||
private ulong _mask; |
|||
|
|||
public int Count => _sources?.Count ?? 0; |
|||
|
|||
public void Add(IStyleActivator activator) |
|||
{ |
|||
_sources ??= new List<IStyleActivator>(); |
|||
_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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
#nullable enable |
|||
|
|||
using System; |
|||
|
|||
namespace Avalonia.Styling.Activators |
|||
{ |
|||
/// <summary>
|
|||
/// Defines a style activator.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 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.
|
|||
/// </remarks>
|
|||
public interface IStyleActivator : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Subscribes to the activator.
|
|||
/// </summary>
|
|||
/// <param name="sink">The listener.</param>
|
|||
/// <param name="tag">An optional tag.</param>
|
|||
void Subscribe(IStyleActivatorSink sink, int tag = 0); |
|||
|
|||
/// <summary>
|
|||
/// Unsubscribes from the activator.
|
|||
/// </summary>
|
|||
void Unsubscribe(IStyleActivatorSink sink); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Styling.Activators |
|||
{ |
|||
/// <summary>
|
|||
/// Receives notifications from an <see cref="IStyleActivator"/>.
|
|||
/// </summary>
|
|||
public interface IStyleActivatorSink |
|||
{ |
|||
/// <summary>
|
|||
/// Called when the subscribed activator value changes.
|
|||
/// </summary>
|
|||
/// <param name="value">The new value.</param>
|
|||
/// <param name="tag">The subscription tag.</param>
|
|||
void OnNext(bool value, int tag); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
#nullable enable |
|||
|
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Styling.Activators |
|||
{ |
|||
internal class OrActivator : StyleActivatorBase, IStyleActivatorSink |
|||
{ |
|||
private List<IStyleActivator>? _sources; |
|||
private ulong _flags; |
|||
private bool _initializing; |
|||
|
|||
public int Count => _sources?.Count ?? 0; |
|||
|
|||
public void Add(IStyleActivator activator) |
|||
{ |
|||
_sources ??= new List<IStyleActivator>(); |
|||
_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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using System; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Styling.Activators |
|||
{ |
|||
internal class PropertyEqualsActivator : StyleActivatorBase, IObserver<object> |
|||
{ |
|||
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<object>.OnCompleted() { } |
|||
void IObserver<object>.OnError(Exception error) { } |
|||
void IObserver<object>.OnNext(object value) => PublishNext(Equals(value, _value)); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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<string> _match; |
|||
private readonly IAvaloniaReadOnlyList<string> _classes; |
|||
|
|||
public StyleClassActivator(IAvaloniaReadOnlyList<string> classes, IList<string> match) |
|||
{ |
|||
_classes = classes; |
|||
_match = match; |
|||
} |
|||
|
|||
public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> 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); |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a setter that has been instanced on a control.
|
|||
/// </summary>
|
|||
public interface ISetterInstance |
|||
{ |
|||
/// <summary>
|
|||
/// Activates the setter.
|
|||
/// </summary>
|
|||
public void Activate(); |
|||
|
|||
/// <summary>
|
|||
/// Deactivates the setter.
|
|||
/// </summary>
|
|||
public void Deactivate(); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a style that has been instanced on a control.
|
|||
/// </summary>
|
|||
public interface IStyleInstance : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the source style.
|
|||
/// </summary>
|
|||
IStyle Source { get; } |
|||
|
|||
/// <summary>
|
|||
/// Instructs the style to start acting upon the control.
|
|||
/// </summary>
|
|||
void Start(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
internal class PropertySetterInstance<T> : ISetterInstance |
|||
{ |
|||
private readonly IStyleable _target; |
|||
private readonly StyledPropertyBase<T>? _styledProperty; |
|||
private readonly DirectPropertyBase<T>? _directProperty; |
|||
private readonly BindingPriority _priority; |
|||
private readonly T _value; |
|||
private IDisposable? _subscription; |
|||
private bool _isActive; |
|||
|
|||
public PropertySetterInstance( |
|||
IStyleable target, |
|||
StyledPropertyBase<T> property, |
|||
BindingPriority priority, |
|||
T value) |
|||
{ |
|||
_target = target; |
|||
_styledProperty = property; |
|||
_priority = priority; |
|||
_value = value; |
|||
} |
|||
|
|||
public PropertySetterInstance( |
|||
IStyleable target, |
|||
DirectPropertyBase<T> 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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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<bool> And(IList<IObservable<bool>> 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<bool> Or(IList<IObservable<bool>> 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(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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<ISetterInstance> _setters; |
|||
private readonly IStyleActivator? _activator; |
|||
private bool _active; |
|||
|
|||
public StyleInstance( |
|||
IStyle source, |
|||
IStyleable target, |
|||
IReadOnlyList<ISetter> 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<ISetterInstance>(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); |
|||
} |
|||
} |
|||
@ -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<bool>(false); |
|||
var source = new BehaviorSubject<object>(1); |
|||
var target = new ActivatedObservable(activator, source, string.Empty); |
|||
var result = new List<object>(); |
|||
|
|||
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<bool>(false); |
|||
var source = new BehaviorSubject<object>(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<bool>(false); |
|||
var source = new BehaviorSubject<object>(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); |
|||
} |
|||
} |
|||
} |
|||
@ -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<bool>(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<bool>(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<bool>(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<object> |
|||
{ |
|||
private IObserver<object> _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<object> observer) |
|||
{ |
|||
_observer = observer; |
|||
return Disposable.Empty; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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<bool>(false); |
|||
var target = new ActivatedValue(activator, 1, string.Empty); |
|||
var result = new List<object>(); |
|||
|
|||
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<bool>(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<bool>(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<bool>(); |
|||
var activator2 = scheduler.CreateColdObservable<bool>(); |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
using System; |
|||
using System.Reactive.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Reactive; |
|||
using Avalonia.Styling.Activators; |
|||
|
|||
namespace Avalonia.Styling.UnitTests |
|||
{ |
|||
internal static class StyleActivatorExtensions |
|||
{ |
|||
public static IDisposable Subscribe(this IStyleActivator activator, Action<bool> action) |
|||
{ |
|||
return activator.ToObservable().Subscribe(action); |
|||
} |
|||
|
|||
public static async Task<bool> Take(this IStyleActivator activator, int value) |
|||
{ |
|||
return await activator.ToObservable().Take(value); |
|||
} |
|||
|
|||
public static IObservable<bool> ToObservable(this IStyleActivator activator) |
|||
{ |
|||
return new ObservableAdapter(activator); |
|||
} |
|||
|
|||
private class ObservableAdapter : LightweightObservableBase<bool>, IStyleActivatorSink |
|||
{ |
|||
private readonly IStyleActivator _source; |
|||
private bool _value; |
|||
|
|||
public ObservableAdapter(IStyleActivator source) => _source = source; |
|||
protected override void Initialize() => _source.Subscribe(this); |
|||
protected override void Deinitialize() => _source.Unsubscribe(this); |
|||
|
|||
void IStyleActivatorSink.OnNext(bool value, int tag) |
|||
{ |
|||
_value = value; |
|||
PublishNext(value); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,169 +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; |
|||
using System.Reactive.Linq; |
|||
using Microsoft.Reactive.Testing; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Styling.UnitTests |
|||
{ |
|||
public class StyleActivatorTests : ReactiveTest |
|||
{ |
|||
[Fact] |
|||
public void Activator_Should_Subscribe_To_Inputs_On_First_Subscription() |
|||
{ |
|||
var scheduler = new TestScheduler(); |
|||
var source = scheduler.CreateColdObservable<bool>(); |
|||
var target = StyleActivator.And(new[] { source }); |
|||
|
|||
Assert.Equal(0, source.Subscriptions.Count); |
|||
target.Subscribe(_ => { }); |
|||
Assert.Equal(1, source.Subscriptions.Count); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Activator_Should_Unsubscribe_From_Inputs_After_Last_Subscriber_Completes() |
|||
{ |
|||
var scheduler = new TestScheduler(); |
|||
var source = scheduler.CreateColdObservable<bool>(); |
|||
var target = StyleActivator.And(new[] { source }); |
|||
|
|||
var dispose = target.Subscribe(_ => { }); |
|||
Assert.Equal(1, source.Subscriptions.Count); |
|||
Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); |
|||
|
|||
dispose.Dispose(); |
|||
Assert.Equal(1, source.Subscriptions.Count); |
|||
Assert.Equal(0, source.Subscriptions[0].Unsubscribe); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Activator_And_Should_Follow_Single_Input() |
|||
{ |
|||
var inputs = new[] { new TestSubject<bool>(false) }; |
|||
var target = StyleActivator.And(inputs); |
|||
var result = new TestObserver<bool>(); |
|||
|
|||
target.Subscribe(result); |
|||
Assert.False(result.GetValue()); |
|||
inputs[0].OnNext(true); |
|||
Assert.True(result.GetValue()); |
|||
inputs[0].OnNext(false); |
|||
Assert.False(result.GetValue()); |
|||
inputs[0].OnNext(true); |
|||
Assert.True(result.GetValue()); |
|||
|
|||
Assert.Equal(1, inputs[0].SubscriberCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Activator_And_Should_AND_Multiple_Inputs() |
|||
{ |
|||
var inputs = new[] |
|||
{ |
|||
new TestSubject<bool>(false), |
|||
new TestSubject<bool>(false), |
|||
new TestSubject<bool>(true), |
|||
}; |
|||
var target = StyleActivator.And(inputs); |
|||
var result = new TestObserver<bool>(); |
|||
|
|||
target.Subscribe(result); |
|||
Assert.False(result.GetValue()); |
|||
inputs[0].OnNext(true); |
|||
inputs[1].OnNext(true); |
|||
Assert.True(result.GetValue()); |
|||
inputs[0].OnNext(false); |
|||
Assert.False(result.GetValue()); |
|||
|
|||
Assert.Equal(1, inputs[0].SubscriberCount); |
|||
Assert.Equal(1, inputs[1].SubscriberCount); |
|||
Assert.Equal(1, inputs[2].SubscriberCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Activator_Or_Should_Follow_Single_Input() |
|||
{ |
|||
var inputs = new[] { new TestSubject<bool>(false) }; |
|||
var target = StyleActivator.Or(inputs); |
|||
var result = new TestObserver<bool>(); |
|||
|
|||
target.Subscribe(result); |
|||
Assert.False(result.GetValue()); |
|||
inputs[0].OnNext(true); |
|||
Assert.True(result.GetValue()); |
|||
inputs[0].OnNext(false); |
|||
Assert.False(result.GetValue()); |
|||
inputs[0].OnNext(true); |
|||
Assert.True(result.GetValue()); |
|||
|
|||
Assert.Equal(1, inputs[0].SubscriberCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Activator_Or_Should_OR_Multiple_Inputs() |
|||
{ |
|||
var inputs = new[] |
|||
{ |
|||
new TestSubject<bool>(false), |
|||
new TestSubject<bool>(false), |
|||
new TestSubject<bool>(true), |
|||
}; |
|||
var target = StyleActivator.Or(inputs); |
|||
var result = new TestObserver<bool>(); |
|||
|
|||
target.Subscribe(result); |
|||
Assert.True(result.GetValue()); |
|||
inputs[2].OnNext(false); |
|||
Assert.False(result.GetValue()); |
|||
inputs[0].OnNext(true); |
|||
Assert.True(result.GetValue()); |
|||
|
|||
Assert.Equal(1, inputs[0].SubscriberCount); |
|||
Assert.Equal(1, inputs[1].SubscriberCount); |
|||
Assert.Equal(1, inputs[2].SubscriberCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Activator_Or_Should_Not_Unsubscribe_All_When_Input_Completes_On_False() |
|||
{ |
|||
var inputs = new[] |
|||
{ |
|||
new TestSubject<bool>(false), |
|||
new TestSubject<bool>(false), |
|||
new TestSubject<bool>(true), |
|||
}; |
|||
var target = StyleActivator.Or(inputs); |
|||
var result = new TestObserver<bool>(); |
|||
|
|||
target.Subscribe(result); |
|||
Assert.True(result.GetValue()); |
|||
inputs[2].OnNext(false); |
|||
Assert.False(result.GetValue()); |
|||
inputs[2].OnCompleted(); |
|||
|
|||
Assert.Equal(1, inputs[0].SubscriberCount); |
|||
Assert.Equal(1, inputs[1].SubscriberCount); |
|||
Assert.Equal(0, inputs[2].SubscriberCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Completed_Activator_Should_Signal_OnCompleted() |
|||
{ |
|||
var inputs = new[] |
|||
{ |
|||
Observable.Return(false), |
|||
}; |
|||
|
|||
var target = StyleActivator.Or(inputs); |
|||
var completed = false; |
|||
|
|||
target.Subscribe(_ => { }, () => completed = true); |
|||
|
|||
Assert.True(completed); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue