committed by
GitHub
116 changed files with 2029 additions and 1686 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; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue