committed by
GitHub
78 changed files with 1886 additions and 1536 deletions
@ -0,0 +1,34 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Utilities |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A visitor to resolve an untyped <see cref="AvaloniaProperty"/> to a typed property.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TData">The type of user data passed.</typeparam>
|
||||
|
/// <remarks>
|
||||
|
/// Pass an instance that implements this interface to
|
||||
|
/// <see cref="AvaloniaProperty.Accept{TData}(IAvaloniaPropertyVisitor{TData}, ref TData)"/>
|
||||
|
/// in order to resolve un untyped <see cref="AvaloniaProperty"/> to a typed
|
||||
|
/// <see cref="StyledPropertyBase{TValue}"/> or <see cref="DirectPropertyBase{TValue}"/>.
|
||||
|
/// </remarks>
|
||||
|
public interface IAvaloniaPropertyVisitor<TData> |
||||
|
where TData : struct |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Called when the property is a styled property.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The property value type.</typeparam>
|
||||
|
/// <param name="property">The property.</param>
|
||||
|
/// <param name="data">The user data.</param>
|
||||
|
void Visit<T>(StyledPropertyBase<T> property, ref TData data); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the property is a direct property.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The property value type.</typeparam>
|
||||
|
/// <param name="property">The property.</param>
|
||||
|
/// <param name="data">The user data.</param>
|
||||
|
void Visit<T>(DirectPropertyBase<T> property, ref TData data); |
||||
|
} |
||||
|
} |
||||
@ -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,71 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An aggregate <see cref="IStyleActivator"/> which is active when all of its inputs are
|
||||
|
/// active.
|
||||
|
/// </summary>
|
||||
|
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,43 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Builds an <see cref="AndActivator"/>.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// When ANDing style activators, if there is more than one input then creates an instance of
|
||||
|
/// <see cref="AndActivator"/>. If there is only one input, returns the input directly.
|
||||
|
/// </remarks>
|
||||
|
internal struct AndActivatorBuilder |
||||
|
{ |
||||
|
private IStyleActivator? _single; |
||||
|
private AndActivator? _multiple; |
||||
|
|
||||
|
public void Add(IStyleActivator? activator) |
||||
|
{ |
||||
|
if (activator == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (_single is null && _multiple is null) |
||||
|
{ |
||||
|
_single = activator; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (_multiple is null) |
||||
|
{ |
||||
|
_multiple = new AndActivator(); |
||||
|
_multiple.Add(_single!); |
||||
|
_single = null; |
||||
|
} |
||||
|
|
||||
|
_multiple.Add(activator); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IStyleActivator Get() => _single ?? _multiple!; |
||||
|
} |
||||
|
} |
||||
@ -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,16 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An <see cref="IStyleActivator"/> which inverts the state of an input activator.
|
||||
|
/// </summary>
|
||||
|
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,71 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An aggregate <see cref="IStyleActivator"/> which is active when any of its inputs are
|
||||
|
/// active.
|
||||
|
/// </summary>
|
||||
|
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,45 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Builds an <see cref="OrActivator"/>.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// When ORing style activators, if there is more than one input then creates an instance of
|
||||
|
/// <see cref="OrActivator"/>. If there is only one input, returns the input directly.
|
||||
|
/// </remarks>
|
||||
|
internal struct OrActivatorBuilder |
||||
|
{ |
||||
|
private IStyleActivator? _single; |
||||
|
private OrActivator? _multiple; |
||||
|
|
||||
|
public int Count => _multiple?.Count ?? (_single is object ? 1 : 0); |
||||
|
|
||||
|
public void Add(IStyleActivator? activator) |
||||
|
{ |
||||
|
if (activator == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (_single is null && _multiple is null) |
||||
|
{ |
||||
|
_single = activator; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (_multiple is null) |
||||
|
{ |
||||
|
_multiple = new OrActivator(); |
||||
|
_multiple.Add(_single!); |
||||
|
_single = null; |
||||
|
} |
||||
|
|
||||
|
_multiple.Add(activator); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IStyleActivator Get() => _single ?? _multiple!; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
using System; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An <see cref="IStyleActivator"/> which listens to a property value on a control.
|
||||
|
/// </summary>
|
||||
|
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,58 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Base class implementation of <see cref="IStyleActivator"/>.
|
||||
|
/// </summary>
|
||||
|
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,76 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.Specialized; |
||||
|
using Avalonia.Collections; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling.Activators |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An <see cref="IStyleActivator"/> which is active when a set of classes match those on a
|
||||
|
/// control.
|
||||
|
/// </summary>
|
||||
|
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,40 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a setter that has been instanced on a control.
|
||||
|
/// </summary>
|
||||
|
public interface ISetterInstance : IDisposable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Starts the setter instance.
|
||||
|
/// </summary>
|
||||
|
/// <param name="hasActivator">Whether the parent style has an activator.</param>
|
||||
|
/// <remarks>
|
||||
|
/// If <paramref name="hasActivator"/> is false then the setter should be immediately
|
||||
|
/// applied and <see cref="Activate"/> and <see cref="Deactivate"/> should not be called.
|
||||
|
/// If true, then bindings etc should be initiated but not produce a value until
|
||||
|
/// <see cref="Activate"/> called.
|
||||
|
/// </remarks>
|
||||
|
public void Start(bool hasActivator); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Activates the setter.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
|
||||
|
/// </remarks>
|
||||
|
public void Activate(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Deactivates the setter.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Should only be called if hasActivator was true when <see cref="Start(bool)"/> was called.
|
||||
|
/// </remarks>
|
||||
|
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,180 @@ |
|||||
|
using System; |
||||
|
using System.Reactive.Subjects; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Reactive; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A <see cref="Setter"/> which has been instanced on a control and has an
|
||||
|
/// <see cref="IBinding"/> as its value.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The target property type.</typeparam>
|
||||
|
internal class PropertySetterBindingInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
||||
|
ISubject<BindingValue<T>>, |
||||
|
ISetterInstance |
||||
|
{ |
||||
|
private readonly IStyleable _target; |
||||
|
private readonly StyledPropertyBase<T>? _styledProperty; |
||||
|
private readonly DirectPropertyBase<T>? _directProperty; |
||||
|
private readonly InstancedBinding _binding; |
||||
|
private readonly Inner _inner; |
||||
|
private BindingValue<T> _value; |
||||
|
private IDisposable? _subscription; |
||||
|
private IDisposable? _subscriptionTwoWay; |
||||
|
private bool _isActive; |
||||
|
|
||||
|
public PropertySetterBindingInstance( |
||||
|
IStyleable target, |
||||
|
StyledPropertyBase<T> property, |
||||
|
IBinding binding) |
||||
|
{ |
||||
|
_target = target; |
||||
|
_styledProperty = property; |
||||
|
_binding = binding.Initiate(_target, property); |
||||
|
|
||||
|
if (_binding.Mode == BindingMode.OneTime) |
||||
|
{ |
||||
|
// For the moment, we don't support OneTime bindings in setters, because I'm not
|
||||
|
// sure what the semantics should be in the case of activation/deactivation.
|
||||
|
throw new NotSupportedException("OneTime bindings are not supported in setters."); |
||||
|
} |
||||
|
|
||||
|
_inner = new Inner(this); |
||||
|
} |
||||
|
|
||||
|
public PropertySetterBindingInstance( |
||||
|
IStyleable target, |
||||
|
DirectPropertyBase<T> property, |
||||
|
IBinding binding) |
||||
|
{ |
||||
|
_target = target; |
||||
|
_directProperty = property; |
||||
|
_binding = binding.Initiate(_target, property); |
||||
|
_inner = new Inner(this); |
||||
|
} |
||||
|
|
||||
|
public void Start(bool hasActivator) |
||||
|
{ |
||||
|
_isActive = !hasActivator; |
||||
|
|
||||
|
if (_styledProperty is object) |
||||
|
{ |
||||
|
if (_binding.Mode != BindingMode.OneWayToSource) |
||||
|
{ |
||||
|
var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; |
||||
|
_subscription = _target.Bind(_styledProperty, this, priority); |
||||
|
} |
||||
|
|
||||
|
if (_binding.Mode == BindingMode.TwoWay) |
||||
|
{ |
||||
|
_subscriptionTwoWay = _target.GetBindingObservable(_styledProperty).Subscribe(this); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (_binding.Mode != BindingMode.OneWayToSource) |
||||
|
{ |
||||
|
_subscription = _target.Bind(_directProperty!, this); |
||||
|
} |
||||
|
|
||||
|
if (_binding.Mode == BindingMode.TwoWay) |
||||
|
{ |
||||
|
_subscriptionTwoWay = _target.GetBindingObservable(_directProperty!).Subscribe(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Activate() |
||||
|
{ |
||||
|
if (!_isActive) |
||||
|
{ |
||||
|
_isActive = true; |
||||
|
PublishNext(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Deactivate() |
||||
|
{ |
||||
|
if (_isActive) |
||||
|
{ |
||||
|
_isActive = false; |
||||
|
PublishNext(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override void Dispose() |
||||
|
{ |
||||
|
if (_subscription is object) |
||||
|
{ |
||||
|
var sub = _subscription; |
||||
|
_subscription = null; |
||||
|
sub.Dispose(); |
||||
|
} |
||||
|
|
||||
|
if (_subscriptionTwoWay is object) |
||||
|
{ |
||||
|
var sub = _subscriptionTwoWay; |
||||
|
_subscriptionTwoWay = null; |
||||
|
sub.Dispose(); |
||||
|
} |
||||
|
|
||||
|
base.Dispose(); |
||||
|
} |
||||
|
|
||||
|
void IObserver<BindingValue<T>>.OnCompleted() |
||||
|
{ |
||||
|
// This is the observable coming from the target control. It should not complete.
|
||||
|
} |
||||
|
|
||||
|
void IObserver<BindingValue<T>>.OnError(Exception error) |
||||
|
{ |
||||
|
// This is the observable coming from the target control. It should not error.
|
||||
|
} |
||||
|
|
||||
|
void IObserver<BindingValue<T>>.OnNext(BindingValue<T> value) |
||||
|
{ |
||||
|
if (value.HasValue && _isActive) |
||||
|
{ |
||||
|
_binding.Subject.OnNext(value.Value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override void Subscribed() |
||||
|
{ |
||||
|
_subscription = _binding.Observable.Subscribe(_inner); |
||||
|
} |
||||
|
|
||||
|
protected override void Unsubscribed() |
||||
|
{ |
||||
|
_subscription?.Dispose(); |
||||
|
_subscription = null; |
||||
|
} |
||||
|
|
||||
|
private void PublishNext() |
||||
|
{ |
||||
|
PublishNext(_isActive ? _value : default); |
||||
|
} |
||||
|
|
||||
|
private void ConvertAndPublishNext(object? value) |
||||
|
{ |
||||
|
_value = value is T v ? v : BindingValue<object>.FromUntyped(value).Convert<T>(); |
||||
|
|
||||
|
if (_isActive) |
||||
|
{ |
||||
|
PublishNext(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class Inner : IObserver<object?> |
||||
|
{ |
||||
|
private readonly PropertySetterBindingInstance<T> _owner; |
||||
|
public Inner(PropertySetterBindingInstance<T> owner) => _owner = owner; |
||||
|
public void OnCompleted() => _owner.PublishCompleted(); |
||||
|
public void OnError(Exception error) => _owner.PublishError(error); |
||||
|
public void OnNext(object? value) => _owner.ConvertAndPublishNext(value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Reactive; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A <see cref="Setter"/> which has been instance on a control.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The target property type.</typeparam>
|
||||
|
internal class PropertySetterInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
||||
|
ISetterInstance |
||||
|
{ |
||||
|
private readonly IStyleable _target; |
||||
|
private readonly StyledPropertyBase<T>? _styledProperty; |
||||
|
private readonly DirectPropertyBase<T>? _directProperty; |
||||
|
private readonly T _value; |
||||
|
private IDisposable? _subscription; |
||||
|
private bool _isActive; |
||||
|
|
||||
|
public PropertySetterInstance( |
||||
|
IStyleable target, |
||||
|
StyledPropertyBase<T> property, |
||||
|
T value) |
||||
|
{ |
||||
|
_target = target; |
||||
|
_styledProperty = property; |
||||
|
_value = value; |
||||
|
} |
||||
|
|
||||
|
public PropertySetterInstance( |
||||
|
IStyleable target, |
||||
|
DirectPropertyBase<T> property, |
||||
|
T value) |
||||
|
{ |
||||
|
_target = target; |
||||
|
_directProperty = property; |
||||
|
_value = value; |
||||
|
} |
||||
|
|
||||
|
public void Start(bool hasActivator) |
||||
|
{ |
||||
|
if (hasActivator) |
||||
|
{ |
||||
|
if (_styledProperty is object) |
||||
|
{ |
||||
|
_subscription = _target.Bind(_styledProperty, this, BindingPriority.StyleTrigger); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_subscription = _target.Bind(_directProperty, this); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (_styledProperty is object) |
||||
|
{ |
||||
|
_subscription = _target.SetValue(_styledProperty, _value, BindingPriority.Style); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_target.SetValue(_directProperty!, _value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Activate() |
||||
|
{ |
||||
|
if (!_isActive) |
||||
|
{ |
||||
|
_isActive = true; |
||||
|
PublishNext(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Deactivate() |
||||
|
{ |
||||
|
if (_isActive) |
||||
|
{ |
||||
|
_isActive = false; |
||||
|
PublishNext(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override void Dispose() |
||||
|
{ |
||||
|
if (_subscription is object) |
||||
|
{ |
||||
|
var sub = _subscription; |
||||
|
_subscription = null; |
||||
|
sub.Dispose(); |
||||
|
} |
||||
|
else if (_isActive) |
||||
|
{ |
||||
|
if (_styledProperty is object) |
||||
|
{ |
||||
|
_target.ClearValue(_styledProperty); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_target.ClearValue(_directProperty); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
base.Dispose(); |
||||
|
} |
||||
|
|
||||
|
protected override void Subscribed() => PublishNext(); |
||||
|
protected override void Unsubscribed() { } |
||||
|
|
||||
|
private void PublishNext() |
||||
|
{ |
||||
|
PublishNext(_isActive ? new BindingValue<T>(_value) : default); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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,135 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Reactive.Subjects; |
||||
|
using Avalonia.Animation; |
||||
|
using Avalonia.Styling.Activators; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A <see cref="Style"/> which has been instanced on a control.
|
||||
|
/// </summary>
|
||||
|
internal class StyleInstance : IStyleInstance, IStyleActivatorSink |
||||
|
{ |
||||
|
private readonly List<ISetterInstance>? _setters; |
||||
|
private readonly List<IDisposable>? _animations; |
||||
|
private readonly IStyleActivator? _activator; |
||||
|
private readonly Subject<bool>? _animationTrigger; |
||||
|
private bool _active; |
||||
|
|
||||
|
public StyleInstance( |
||||
|
IStyle source, |
||||
|
IStyleable target, |
||||
|
IReadOnlyList<ISetter>? setters, |
||||
|
IReadOnlyList<IAnimation>? animations, |
||||
|
IStyleActivator? activator = null) |
||||
|
{ |
||||
|
Source = source ?? throw new ArgumentNullException(nameof(source)); |
||||
|
Target = target ?? throw new ArgumentNullException(nameof(target)); |
||||
|
_activator = activator; |
||||
|
|
||||
|
if (setters is object) |
||||
|
{ |
||||
|
var setterCount = setters.Count; |
||||
|
|
||||
|
_setters = new List<ISetterInstance>(setterCount); |
||||
|
|
||||
|
for (var i = 0; i < setterCount; ++i) |
||||
|
{ |
||||
|
_setters.Add(setters[i].Instance(Target)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (animations is object && target is Animatable animatable) |
||||
|
{ |
||||
|
var animationsCount = animations.Count; |
||||
|
|
||||
|
_animations = new List<IDisposable>(animationsCount); |
||||
|
_animationTrigger = new Subject<bool>(); |
||||
|
|
||||
|
for (var i = 0; i < animationsCount; ++i) |
||||
|
{ |
||||
|
_animations.Add(animations[i].Apply(animatable, null, _animationTrigger)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IStyle Source { get; } |
||||
|
public IStyleable Target { get; } |
||||
|
|
||||
|
public void Start() |
||||
|
{ |
||||
|
var hasActivator = _activator is object; |
||||
|
|
||||
|
if (_setters is object) |
||||
|
{ |
||||
|
foreach (var setter in _setters) |
||||
|
{ |
||||
|
setter.Start(hasActivator); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (hasActivator) |
||||
|
{ |
||||
|
_activator!.Subscribe(this, 0); |
||||
|
} |
||||
|
else if (_animationTrigger != null) |
||||
|
{ |
||||
|
_animationTrigger.OnNext(true); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if (_setters is object) |
||||
|
{ |
||||
|
foreach (var setter in _setters) |
||||
|
{ |
||||
|
setter.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (_animations is object) |
||||
|
{ |
||||
|
foreach (var subscripion in _animations) |
||||
|
{ |
||||
|
subscripion.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_activator?.Dispose(); |
||||
|
} |
||||
|
|
||||
|
private void ActivatorChanged(bool value) |
||||
|
{ |
||||
|
if (_active != value) |
||||
|
{ |
||||
|
_active = value; |
||||
|
|
||||
|
_animationTrigger?.OnNext(value); |
||||
|
|
||||
|
if (_setters is object) |
||||
|
{ |
||||
|
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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using Avalonia.Media; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Extensions for <see cref="AvaloniaProperty"/>.
|
||||
|
/// </summary>
|
||||
|
public static class AvaloniaPropertyExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Checks if values of given property can affect rendering (via <see cref="IAffectsRender"/>).
|
||||
|
/// </summary>
|
||||
|
/// <param name="property">Property to check.</param>
|
||||
|
public static bool CanValueAffectRender(this AvaloniaProperty property) |
||||
|
{ |
||||
|
var propertyType = property.PropertyType; |
||||
|
|
||||
|
// Only case that we are sure that property value CAN'T affect render are sealed types that don't implement
|
||||
|
// the interface.
|
||||
|
var cannotAffectRender = propertyType.IsSealed && !typeof(IAffectsRender).IsAssignableFrom(propertyType); |
||||
|
|
||||
|
return !cannotAffectRender; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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