mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-15 03:20:30 +00:00
1997 lines
80 KiB
C#
1997 lines
80 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
#nullable disable
|
|
|
|
using osu.Framework.Lists;
|
|
using System.Collections.Generic;
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Runtime.ExceptionServices;
|
|
using System.Threading;
|
|
using osuTK;
|
|
using osuTK.Graphics;
|
|
using osu.Framework.Graphics.Shaders;
|
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
using osu.Framework.Graphics.Colour;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Graphics.Transforms;
|
|
using osu.Framework.Timing;
|
|
using osu.Framework.Threading;
|
|
using osu.Framework.Statistics;
|
|
using System.Threading.Tasks;
|
|
using JetBrains.Annotations;
|
|
using osu.Framework.Development;
|
|
using osu.Framework.Extensions.EnumExtensions;
|
|
using osu.Framework.Extensions.ExceptionExtensions;
|
|
using osu.Framework.Graphics.Effects;
|
|
using osu.Framework.Graphics.Primitives;
|
|
using osu.Framework.Layout;
|
|
using osu.Framework.Logging;
|
|
using osu.Framework.Utils;
|
|
|
|
namespace osu.Framework.Graphics.Containers
|
|
{
|
|
/// <summary>
|
|
/// A drawable consisting of a composite of child drawables which are
|
|
/// manages by the composite object itself. Transformations applied to
|
|
/// a <see cref="CompositeDrawable"/> are also applied to its children.
|
|
/// Additionally, <see cref="CompositeDrawable"/>s support various effects, such as masking, edge effect,
|
|
/// padding, and automatic sizing depending on their children.
|
|
/// </summary>
|
|
public abstract partial class CompositeDrawable : Drawable
|
|
{
|
|
#region Construction and disposal
|
|
|
|
/// <summary>
|
|
/// Constructs a <see cref="CompositeDrawable"/> that stores children.
|
|
/// </summary>
|
|
protected CompositeDrawable()
|
|
{
|
|
var childComparer = new ChildComparer(this);
|
|
|
|
internalChildren = new SortedList<Drawable>(childComparer);
|
|
aliveInternalChildren = new SortedList<Drawable>(childComparer);
|
|
|
|
// Validate initially. Done before the layout is added below to prevent a callback to this composite.
|
|
childrenSizeDependencies.Validate();
|
|
|
|
AddLayout(childrenSizeDependencies);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a local dependency container which will be used by our nested children.
|
|
/// If not overridden, the load-time parent's dependency tree will be used.
|
|
/// </summary>
|
|
/// <param name="parent">The parent <see cref="IReadOnlyDependencyContainer"/> which should be passed through if we want fallback lookups to work.</param>
|
|
/// <returns>A new dependency container to be stored for this Drawable.</returns>
|
|
protected virtual IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => DependencyActivator.MergeDependencies(this, parent);
|
|
|
|
/// <summary>
|
|
/// Contains all dependencies that can be injected into this CompositeDrawable's children using <see cref="BackgroundDependencyLoaderAttribute"/>.
|
|
/// Add or override dependencies by calling <see cref="DependencyContainer.Cache(object)"/>.
|
|
/// </summary>
|
|
public IReadOnlyDependencyContainer Dependencies { get; private set; }
|
|
|
|
protected sealed override void InjectDependencies(IReadOnlyDependencyContainer dependencies)
|
|
{
|
|
// get our dependencies from our parent, but allow local overriding of our inherited dependency container
|
|
Dependencies = CreateChildDependencies(dependencies);
|
|
|
|
base.InjectDependencies(dependencies);
|
|
}
|
|
|
|
private CancellationTokenSource disposalCancellationSource;
|
|
|
|
private WeakList<Drawable> loadingComponents;
|
|
|
|
internal static readonly ThreadedTaskScheduler SCHEDULER_STANDARD = new ThreadedTaskScheduler(4, $"{nameof(LoadComponentsAsync)} (standard)");
|
|
|
|
internal static readonly ThreadedTaskScheduler SCHEDULER_LONG_LOAD = new ThreadedTaskScheduler(4, $"{nameof(LoadComponentsAsync)} (long load)");
|
|
|
|
/// <summary>
|
|
/// Loads a future child or grand-child of this <see cref="CompositeDrawable"/> asynchronously. <see cref="Dependencies"/>
|
|
/// and <see cref="Drawable.Clock"/> are inherited from this <see cref="CompositeDrawable"/>.
|
|
///
|
|
/// Note that this will always use the dependencies and clock from this instance. If you must load to a nested container level,
|
|
/// consider using <see cref="DelayedLoadWrapper"/>
|
|
/// </summary>
|
|
/// <typeparam name="TLoadable">The type of the future future child or grand-child to be loaded.</typeparam>
|
|
/// <param name="component">The child or grand-child to be loaded.</param>
|
|
/// <param name="onLoaded">Callback to be invoked on the update thread after loading is complete.</param>
|
|
/// <param name="cancellation">An optional cancellation token.</param>
|
|
/// <param name="scheduler">The scheduler for <paramref name="onLoaded"/> to be invoked on. If null, the local scheduler will be used.</param>
|
|
/// <returns>The task which is used for loading and callbacks.</returns>
|
|
protected internal Task LoadComponentAsync<TLoadable>([NotNull] TLoadable component, Action<TLoadable> onLoaded = null, CancellationToken cancellation = default, Scheduler scheduler = null)
|
|
where TLoadable : Drawable
|
|
{
|
|
ArgumentNullException.ThrowIfNull(component);
|
|
|
|
return LoadComponentsAsync(component.Yield(), l => onLoaded?.Invoke(l.Single()), cancellation, scheduler);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads a future child or grand-child of this <see cref="CompositeDrawable"/> synchronously and immediately. <see cref="Dependencies"/>
|
|
/// and <see cref="Drawable.Clock"/> are inherited from this <see cref="CompositeDrawable"/>.
|
|
/// <remarks>
|
|
/// This is generally useful if already in an asynchronous context and requiring forcefully (pre)loading content without adding it to the hierarchy.
|
|
/// </remarks>
|
|
/// </summary>
|
|
/// <typeparam name="TLoadable">The type of the future future child or grand-child to be loaded.</typeparam>
|
|
/// <param name="component">The child or grand-child to be loaded.</param>
|
|
protected void LoadComponent<TLoadable>(TLoadable component) where TLoadable : Drawable
|
|
=> LoadComponents(component.Yield());
|
|
|
|
/// <summary>
|
|
/// Loads several future child or grand-child of this <see cref="CompositeDrawable"/> asynchronously. <see cref="Dependencies"/>
|
|
/// and <see cref="Drawable.Clock"/> are inherited from this <see cref="CompositeDrawable"/>.
|
|
///
|
|
/// Note that this will always use the dependencies and clock from this instance. If you must load to a nested container level,
|
|
/// consider using <see cref="DelayedLoadWrapper"/>
|
|
/// </summary>
|
|
/// <typeparam name="TLoadable">The type of the future future child or grand-child to be loaded.</typeparam>
|
|
/// <param name="components">The children or grand-children to be loaded.</param>
|
|
/// <param name="onLoaded">Callback to be invoked on the update thread after loading is complete.</param>
|
|
/// <param name="cancellation">An optional cancellation token.</param>
|
|
/// <param name="scheduler">The scheduler for <paramref name="onLoaded"/> to be invoked on. If null, the local scheduler will be used.</param>
|
|
/// <returns>The task which is used for loading and callbacks.</returns>
|
|
protected internal Task LoadComponentsAsync<TLoadable>(IEnumerable<TLoadable> components, Action<IEnumerable<TLoadable>> onLoaded = null, CancellationToken cancellation = default,
|
|
Scheduler scheduler = null)
|
|
where TLoadable : Drawable
|
|
{
|
|
if (LoadState < LoadState.Loading)
|
|
throw new InvalidOperationException($"May not invoke {nameof(LoadComponentAsync)} prior to this {nameof(CompositeDrawable)} being loaded.");
|
|
|
|
EnsureMutationAllowed($"load components via {nameof(LoadComponentsAsync)}");
|
|
|
|
if (IsDisposed)
|
|
throw new ObjectDisposedException(ToString());
|
|
|
|
disposalCancellationSource ??= new CancellationTokenSource();
|
|
|
|
var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(disposalCancellationSource.Token, cancellation);
|
|
|
|
var deps = new DependencyContainer(Dependencies);
|
|
deps.CacheValueAs(linkedSource.Token);
|
|
|
|
loadingComponents ??= new WeakList<Drawable>();
|
|
|
|
var loadables = components.ToList();
|
|
|
|
foreach (var d in loadables)
|
|
{
|
|
loadingComponents.Add(d);
|
|
LoadingComponentsLogger.Add(d);
|
|
|
|
d.OnLoadComplete += _ =>
|
|
{
|
|
loadingComponents.Remove(d);
|
|
LoadingComponentsLogger.Remove(d);
|
|
};
|
|
}
|
|
|
|
var taskScheduler = loadables.Any(c => c.IsLongRunning) ? SCHEDULER_LONG_LOAD : SCHEDULER_STANDARD;
|
|
|
|
return Task.Factory.StartNew(() => loadComponents(loadables, deps, true, linkedSource.Token), linkedSource.Token, TaskCreationOptions.HideScheduler, taskScheduler).ContinueWith(loaded =>
|
|
{
|
|
var exception = loaded.Exception?.AsSingular();
|
|
|
|
if (loadables.Count == 0)
|
|
return;
|
|
|
|
if (linkedSource.Token.IsCancellationRequested)
|
|
{
|
|
// In the case of cancellation the final load state will not be reached, so cleanup here is required.
|
|
foreach (var d in loadables)
|
|
LoadingComponentsLogger.Remove(d);
|
|
|
|
linkedSource.Dispose();
|
|
return;
|
|
}
|
|
|
|
(scheduler ?? Scheduler).Add(() =>
|
|
{
|
|
try
|
|
{
|
|
if (exception != null)
|
|
ExceptionDispatchInfo.Capture(exception).Throw();
|
|
|
|
if (!linkedSource.Token.IsCancellationRequested)
|
|
onLoaded?.Invoke(loadables);
|
|
}
|
|
finally
|
|
{
|
|
linkedSource.Dispose();
|
|
}
|
|
});
|
|
}, TaskContinuationOptions.ExecuteSynchronously);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads several future child or grand-child of this <see cref="CompositeDrawable"/> synchronously and immediately. <see cref="Dependencies"/>
|
|
/// and <see cref="Drawable.Clock"/> are inherited from this <see cref="CompositeDrawable"/>.
|
|
/// <remarks>
|
|
/// This is generally useful if already in an asynchronous context and requiring forcefully (pre)loading content without adding it to the hierarchy.
|
|
/// </remarks>
|
|
/// </summary>
|
|
/// <typeparam name="TLoadable">The type of the future future child or grand-child to be loaded.</typeparam>
|
|
/// <param name="components">The children or grand-children to be loaded.</param>
|
|
protected void LoadComponents<TLoadable>(IEnumerable<TLoadable> components) where TLoadable : Drawable
|
|
{
|
|
if (LoadState < LoadState.Loading)
|
|
throw new InvalidOperationException($"May not invoke {nameof(LoadComponent)} prior to this {nameof(CompositeDrawable)} being loaded.");
|
|
|
|
if (IsDisposed)
|
|
throw new ObjectDisposedException(ToString());
|
|
|
|
loadComponents(components.ToList(), Dependencies, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load the provided components. Any components which could not be loaded will be removed from the provided list.
|
|
/// </summary>
|
|
private void loadComponents<TLoadable>(List<TLoadable> components, IReadOnlyDependencyContainer dependencies, bool isDirectAsyncContext, CancellationToken cancellation = default)
|
|
where TLoadable : Drawable
|
|
{
|
|
for (int i = 0; i < components.Count; i++)
|
|
{
|
|
if (cancellation.IsCancellationRequested)
|
|
break;
|
|
|
|
if (!components[i].LoadFromAsync(Clock, dependencies, isDirectAsyncContext))
|
|
{
|
|
LoadingComponentsLogger.Remove(components[i]);
|
|
components.Remove(components[i--]);
|
|
}
|
|
}
|
|
}
|
|
|
|
[BackgroundDependencyLoader(true)]
|
|
private void load(ShaderManager shaders, CancellationToken? cancellation)
|
|
{
|
|
hasCustomDrawNode = GetType().GetMethod(nameof(CreateDrawNode))?.DeclaringType != typeof(CompositeDrawable);
|
|
|
|
Shader ??= shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
|
|
|
|
// We are in a potentially async context, so let's aggressively load all our children
|
|
// regardless of their alive state. this also gives children a clock so they can be checked
|
|
// for their correct alive state in the case LifetimeStart is set to a definite value.
|
|
foreach (var c in internalChildren)
|
|
{
|
|
cancellation?.ThrowIfCancellationRequested();
|
|
loadChild(c);
|
|
}
|
|
}
|
|
|
|
protected override void LoadAsyncComplete()
|
|
{
|
|
base.LoadAsyncComplete();
|
|
|
|
// At this point we can assume that we are loaded although we're not in the "ready" state, because we'll be given
|
|
// a "ready" state soon after this method terminates. Therefore we can perform an early check to add any alive children
|
|
// while we're still in an asynchronous context and avoid putting pressure on the main thread during UpdateSubTree.
|
|
CheckChildrenLife();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads a <see cref="Drawable"/> child. This will not throw in the event of the load being cancelled.
|
|
/// </summary>
|
|
/// <param name="child">The <see cref="Drawable"/> child to load.</param>
|
|
private void loadChild(Drawable child)
|
|
{
|
|
try
|
|
{
|
|
if (IsDisposed)
|
|
return;
|
|
|
|
child.Load(Clock, Dependencies, false);
|
|
|
|
child.Parent = this;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
catch (AggregateException ae)
|
|
{
|
|
foreach (var e in ae.Flatten().InnerExceptions)
|
|
{
|
|
if (e is OperationCanceledException)
|
|
continue;
|
|
|
|
ExceptionDispatchInfo.Capture(e).Throw();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void Dispose(bool isDisposing)
|
|
{
|
|
if (IsDisposed)
|
|
return;
|
|
|
|
disposalCancellationSource?.Cancel();
|
|
disposalCancellationSource?.Dispose();
|
|
|
|
InternalChildren?.ForEach(c => c.Dispose());
|
|
|
|
if (loadingComponents != null)
|
|
{
|
|
foreach (var d in loadingComponents)
|
|
{
|
|
d.Dispose();
|
|
LoadingComponentsLogger.Remove(d);
|
|
}
|
|
}
|
|
|
|
OnAutoSize = null;
|
|
Dependencies = null;
|
|
schedulerAfterChildren = null;
|
|
|
|
base.Dispose(isDisposing);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Children management
|
|
|
|
/// <summary>
|
|
/// Invoked when a child has entered <see cref="AliveInternalChildren"/>.
|
|
/// </summary>
|
|
internal event Action<Drawable> ChildBecameAlive;
|
|
|
|
/// <summary>
|
|
/// Invoked when a child has left <see cref="AliveInternalChildren"/>.
|
|
/// </summary>
|
|
internal event Action<Drawable> ChildDied;
|
|
|
|
/// <summary>
|
|
/// Fired after a child's <see cref="Drawable.Depth"/> is changed.
|
|
/// </summary>
|
|
internal event Action<Drawable> ChildDepthChanged;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the only child in <see cref="InternalChildren"/>.
|
|
/// </summary>
|
|
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
|
protected internal Drawable InternalChild
|
|
{
|
|
get
|
|
{
|
|
if (InternalChildren.Count != 1)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Cannot call {nameof(InternalChild)} unless there's exactly one {nameof(Drawable)} in {nameof(InternalChildren)} (currently {InternalChildren.Count})!");
|
|
}
|
|
|
|
return InternalChildren[0];
|
|
}
|
|
set
|
|
{
|
|
if (IsDisposed)
|
|
return;
|
|
|
|
ClearInternal();
|
|
AddInternal(value);
|
|
}
|
|
}
|
|
|
|
protected class ChildComparer : IComparer<Drawable>
|
|
{
|
|
private readonly CompositeDrawable owner;
|
|
|
|
public ChildComparer(CompositeDrawable owner)
|
|
{
|
|
this.owner = owner;
|
|
}
|
|
|
|
public int Compare(Drawable x, Drawable y) => owner.Compare(x, y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compares two <see cref="InternalChildren"/> to determine their sorting.
|
|
/// </summary>
|
|
/// <param name="x">The first child to compare.</param>
|
|
/// <param name="y">The second child to compare.</param>
|
|
/// <returns>-1 if <paramref name="x"/> comes before <paramref name="y"/>, and 1 otherwise.</returns>
|
|
protected virtual int Compare(Drawable x, Drawable y)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(x);
|
|
ArgumentNullException.ThrowIfNull(y);
|
|
|
|
int i = y.Depth.CompareTo(x.Depth);
|
|
if (i != 0) return i;
|
|
|
|
return x.ChildID.CompareTo(y.ChildID);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method comparing children by their depth first, and then by their reversed child ID.
|
|
/// </summary>
|
|
/// <param name="x">The first child to compare.</param>
|
|
/// <param name="y">The second child to compare.</param>
|
|
/// <returns>-1 if <paramref name="x"/> comes before <paramref name="y"/>, and 1 otherwise.</returns>
|
|
protected int CompareReverseChildID(Drawable x, Drawable y)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(x);
|
|
ArgumentNullException.ThrowIfNull(y);
|
|
|
|
int i = y.Depth.CompareTo(x.Depth);
|
|
if (i != 0) return i;
|
|
|
|
return y.ChildID.CompareTo(x.ChildID);
|
|
}
|
|
|
|
private readonly SortedList<Drawable> internalChildren;
|
|
|
|
/// <summary>
|
|
/// This <see cref="CompositeDrawable"/> list of children. Assigning to this property will dispose all existing children of this <see cref="CompositeDrawable"/>.
|
|
/// </summary>
|
|
protected internal IReadOnlyList<Drawable> InternalChildren
|
|
{
|
|
get => internalChildren;
|
|
set => InternalChildrenEnumerable = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replaces all internal children of this <see cref="CompositeDrawable"/> with the elements contained in the enumerable.
|
|
/// </summary>
|
|
protected internal IEnumerable<Drawable> InternalChildrenEnumerable
|
|
{
|
|
set
|
|
{
|
|
if (IsDisposed)
|
|
return;
|
|
|
|
ClearInternal();
|
|
AddRangeInternal(value);
|
|
}
|
|
}
|
|
|
|
private readonly SortedList<Drawable> aliveInternalChildren;
|
|
protected internal IReadOnlyList<Drawable> AliveInternalChildren => aliveInternalChildren;
|
|
|
|
/// <summary>
|
|
/// The index of a given child within <see cref="InternalChildren"/>.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// If the child is found, its index. Otherwise, the negated index it would obtain
|
|
/// if it were added to <see cref="InternalChildren"/>.
|
|
/// </returns>
|
|
/// <exception cref="InvalidOperationException">
|
|
/// <list type="bullet">
|
|
/// <item>If the supplied <paramref name="drawable"/> is already attached to another <see cref="Drawable.Parent"/>.</item>
|
|
/// <item>If a child drawable was matched using <see cref="Compare"/>, but that child drawable was not the supplied <paramref name="drawable"/>.</item>
|
|
/// </list>
|
|
/// </exception>
|
|
protected internal int IndexOfInternal(Drawable drawable)
|
|
{
|
|
if (drawable.Parent != null && drawable.Parent != this)
|
|
throw new InvalidOperationException($@"Cannot call {nameof(IndexOfInternal)} for a drawable that already is a child of a different parent.");
|
|
|
|
int index = internalChildren.IndexOf(drawable);
|
|
|
|
if (index >= 0 && internalChildren[index].ChildID != drawable.ChildID)
|
|
throw new InvalidOperationException($@"A non-matching {nameof(Drawable)} was returned. Please ensure {GetType()}'s {nameof(Compare)} override implements a stable sort algorithm.");
|
|
|
|
return index;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether a given child is contained within <see cref="InternalChildren"/>.
|
|
/// </summary>
|
|
protected internal bool ContainsInternal(Drawable drawable) => IndexOfInternal(drawable) >= 0;
|
|
|
|
/// <summary>
|
|
/// Removes a given child from this <see cref="InternalChildren"/>.
|
|
/// </summary>
|
|
/// <param name="drawable">The <see cref="Drawable"/> to be removed.</param>
|
|
/// <param name="disposeImmediately">Whether removed item should be immediately disposed.</param>
|
|
/// <returns>False if <paramref name="drawable"/> was not a child of this <see cref="CompositeDrawable"/> and true otherwise.</returns>
|
|
protected internal virtual bool RemoveInternal(Drawable drawable, bool disposeImmediately)
|
|
{
|
|
try
|
|
{
|
|
EnsureChildMutationAllowed();
|
|
|
|
ArgumentNullException.ThrowIfNull(drawable);
|
|
|
|
int index = IndexOfInternal(drawable);
|
|
if (index < 0)
|
|
return false;
|
|
|
|
internalChildren.RemoveAt(index);
|
|
|
|
if (drawable.IsAlive)
|
|
{
|
|
aliveInternalChildren.Remove(drawable);
|
|
ChildDied?.Invoke(drawable);
|
|
}
|
|
|
|
if (drawable.LoadState >= LoadState.Ready && drawable.Parent != this)
|
|
throw new InvalidOperationException($@"Removed a drawable ({drawable}) whose parent was not this ({this}), but {drawable.Parent}.");
|
|
|
|
drawable.Parent = null;
|
|
drawable.IsAlive = false;
|
|
|
|
if (AutoSizeAxes != Axes.None)
|
|
Invalidate(Invalidation.RequiredParentSizeToFit, InvalidationSource.Child);
|
|
|
|
return true;
|
|
}
|
|
finally
|
|
{
|
|
if (disposeImmediately)
|
|
drawable?.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear all of <see cref="InternalChildren"/>.
|
|
/// </summary>
|
|
/// <param name="disposeChildren">
|
|
/// Whether removed children should also get disposed.
|
|
/// Disposal will be recursive.
|
|
/// </param>
|
|
protected internal virtual void ClearInternal(bool disposeChildren = true)
|
|
{
|
|
EnsureChildMutationAllowed();
|
|
|
|
if (IsDisposed)
|
|
return;
|
|
|
|
if (internalChildren.Count == 0) return;
|
|
|
|
foreach (Drawable t in internalChildren)
|
|
{
|
|
if (t.IsAlive)
|
|
ChildDied?.Invoke(t);
|
|
|
|
t.IsAlive = false;
|
|
t.Parent = null;
|
|
|
|
if (disposeChildren)
|
|
DisposeChildAsync(t);
|
|
|
|
Trace.Assert(t.Parent == null);
|
|
}
|
|
|
|
internalChildren.Clear();
|
|
aliveInternalChildren.Clear();
|
|
RequestsNonPositionalInputSubTree = RequestsNonPositionalInput;
|
|
RequestsPositionalInputSubTree = RequestsPositionalInput;
|
|
|
|
if (AutoSizeAxes != Axes.None)
|
|
Invalidate(Invalidation.RequiredParentSizeToFit, InvalidationSource.Child);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used to assign a monotonically increasing ID to children as they are added. This member is
|
|
/// incremented whenever a child is added.
|
|
/// </summary>
|
|
private ulong currentChildID;
|
|
|
|
/// <summary>
|
|
/// Adds a child to <see cref="InternalChildren"/>.
|
|
/// </summary>
|
|
protected virtual void AddInternal(Drawable drawable)
|
|
{
|
|
EnsureChildMutationAllowed();
|
|
|
|
if (IsDisposed)
|
|
return;
|
|
|
|
if (drawable == null)
|
|
throw new ArgumentNullException(nameof(drawable), $"null {nameof(Drawable)}s may not be added to {nameof(CompositeDrawable)}.");
|
|
|
|
if (drawable == this)
|
|
throw new InvalidOperationException($"{nameof(CompositeDrawable)} may not be added to itself.");
|
|
|
|
// If the drawable's ChildId is not zero, then it was added to another parent even if it wasn't loaded
|
|
if (drawable.ChildID != 0)
|
|
throw new InvalidOperationException("May not add a drawable to multiple containers.");
|
|
|
|
drawable.ChildID = ++currentChildID;
|
|
drawable.RemoveCompletedTransforms = RemoveCompletedTransforms;
|
|
|
|
if (LoadState >= LoadState.Loading)
|
|
{
|
|
// If we're already loaded, we can eagerly allow children to be loaded
|
|
|
|
if (drawable.LoadState >= LoadState.Ready)
|
|
drawable.Parent = this;
|
|
else
|
|
loadChild(drawable);
|
|
}
|
|
|
|
internalChildren.Add(drawable);
|
|
|
|
if (AutoSizeAxes != Axes.None)
|
|
Invalidate(Invalidation.RequiredParentSizeToFit, InvalidationSource.Child);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a range of children to <see cref="InternalChildren"/>. This is equivalent to calling
|
|
/// <see cref="AddInternal(Drawable)"/> on each element of the range in order.
|
|
/// </summary>
|
|
protected internal void AddRangeInternal(IEnumerable<Drawable> range)
|
|
{
|
|
if (range is IContainerEnumerable<Drawable>)
|
|
{
|
|
throw new InvalidOperationException($"Attempting to add a {nameof(IContainer)} as a range of children to {this}."
|
|
+ $"If intentional, consider using the {nameof(IContainerEnumerable<Drawable>.Children)} property instead.");
|
|
}
|
|
|
|
foreach (Drawable d in range)
|
|
AddInternal(d);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Changes the depth of an internal child. This affects ordering of <see cref="InternalChildren"/>.
|
|
/// </summary>
|
|
/// <param name="child">The child whose depth is to be changed.</param>
|
|
/// <param name="newDepth">The new depth value to be set.</param>
|
|
protected internal void ChangeInternalChildDepth(Drawable child, float newDepth)
|
|
{
|
|
EnsureChildMutationAllowed();
|
|
|
|
if (child.Depth == newDepth) return;
|
|
|
|
int index = IndexOfInternal(child);
|
|
if (index < 0)
|
|
throw new InvalidOperationException($"Can not change depth of drawable which is not contained within this {nameof(CompositeDrawable)}.");
|
|
|
|
internalChildren.RemoveAt(index);
|
|
int aliveIndex = aliveInternalChildren.IndexOf(child);
|
|
if (aliveIndex >= 0) // remove if found
|
|
aliveInternalChildren.RemoveAt(aliveIndex);
|
|
|
|
ulong chId = child.ChildID;
|
|
child.ChildID = 0; // ensure Depth-change does not throw an exception
|
|
child.Depth = newDepth;
|
|
child.ChildID = chId;
|
|
|
|
internalChildren.Add(child);
|
|
if (aliveIndex >= 0) // re-add if it used to be in aliveInternalChildren
|
|
aliveInternalChildren.Add(child);
|
|
|
|
ChildDepthChanged?.Invoke(child);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sorts all children of this <see cref="CompositeDrawable"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This can be used to re-sort the children if the result of <see cref="Compare"/> has changed.
|
|
/// </remarks>
|
|
protected internal void SortInternal()
|
|
{
|
|
EnsureChildMutationAllowed();
|
|
|
|
internalChildren.Sort();
|
|
aliveInternalChildren.Sort();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Updating (per-frame periodic)
|
|
|
|
private Scheduler schedulerAfterChildren;
|
|
|
|
/// <summary>
|
|
/// A lazily-initialized scheduler used to schedule tasks to be invoked in future <see cref="UpdateAfterChildren"/>s calls.
|
|
/// The tasks are invoked at the beginning of the <see cref="UpdateAfterChildren"/> method before anything else.
|
|
/// </summary>
|
|
protected internal Scheduler SchedulerAfterChildren
|
|
{
|
|
get
|
|
{
|
|
if (schedulerAfterChildren != null)
|
|
return schedulerAfterChildren;
|
|
|
|
lock (LoadLock)
|
|
return schedulerAfterChildren ??= new Scheduler(() => ThreadSafety.IsUpdateThread, Clock);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the life status of <see cref="InternalChildren"/> according to their
|
|
/// <see cref="Drawable.ShouldBeAlive"/> property.
|
|
/// </summary>
|
|
/// <returns>True iff the life status of at least one child changed.</returns>
|
|
protected virtual bool UpdateChildrenLife()
|
|
{
|
|
// Can not have alive children if we are not loaded.
|
|
if (LoadState < LoadState.Ready)
|
|
return false;
|
|
|
|
if (!CheckChildrenLife())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the alive state of any child has changed and processes it. This will add or remove
|
|
/// children from <see cref="aliveInternalChildren"/> depending on their alive states.
|
|
/// <para>Note that this does NOT check the load state of this <see cref="CompositeDrawable"/> to check if it can hold any alive children.</para>
|
|
/// </summary>
|
|
/// <returns>Whether any child's alive state has changed.</returns>
|
|
protected virtual bool CheckChildrenLife()
|
|
{
|
|
bool anyAliveChanged = false;
|
|
|
|
for (int i = 0; i < internalChildren.Count; i++)
|
|
{
|
|
var state = checkChildLife(internalChildren[i]);
|
|
|
|
anyAliveChanged |= state.HasFlagFast(ChildLifeStateChange.MadeAlive) || state.HasFlagFast(ChildLifeStateChange.MadeDead);
|
|
|
|
if (state.HasFlagFast(ChildLifeStateChange.Removed))
|
|
i--;
|
|
}
|
|
|
|
FrameStatistics.Add(StatisticsCounterType.CCL, internalChildren.Count);
|
|
|
|
return anyAliveChanged;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the alive state of a child has changed and processes it. This will add or remove
|
|
/// the child from <see cref="aliveInternalChildren"/> depending on its alive state.
|
|
///
|
|
/// This should only ever be called on a <see cref="CompositeDrawable"/>'s own <see cref="internalChildren"/>.
|
|
///
|
|
/// <para>Note that this does NOT check the load state of this <see cref="CompositeDrawable"/> to check if it can hold any alive children.</para>
|
|
/// </summary>
|
|
/// <param name="child">The child to check.</param>
|
|
/// <returns>Whether the child's alive state has changed.</returns>
|
|
private ChildLifeStateChange checkChildLife(Drawable child)
|
|
{
|
|
ChildLifeStateChange state = ChildLifeStateChange.None;
|
|
|
|
if (child.ShouldBeAlive)
|
|
{
|
|
if (!child.IsAlive)
|
|
{
|
|
if (child.LoadState < LoadState.Ready)
|
|
{
|
|
// If we're already loaded, we can eagerly allow children to be loaded
|
|
loadChild(child);
|
|
if (child.LoadState < LoadState.Ready)
|
|
return ChildLifeStateChange.None;
|
|
}
|
|
|
|
MakeChildAlive(child);
|
|
state = ChildLifeStateChange.MadeAlive;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (child.IsAlive || child.RemoveWhenNotAlive)
|
|
{
|
|
if (MakeChildDead(child))
|
|
state |= ChildLifeStateChange.Removed;
|
|
|
|
state |= ChildLifeStateChange.MadeDead;
|
|
}
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
[Flags]
|
|
private enum ChildLifeStateChange
|
|
{
|
|
None = 0,
|
|
MadeAlive = 1,
|
|
MadeDead = 1 << 1,
|
|
Removed = 1 << 2,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Makes a child alive.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Callers have to ensure that <paramref name="child"/> is of this <see cref="CompositeDrawable"/>'s non-alive <see cref="InternalChildren"/> and <see cref="LoadState"/> of the <paramref name="child"/> is at least <see cref="LoadState.Ready"/>.
|
|
/// </remarks>
|
|
/// <param name="child">The child of this <see cref="CompositeDrawable"/>> to make alive.</param>
|
|
protected void MakeChildAlive(Drawable child)
|
|
{
|
|
Debug.Assert(!child.IsAlive && child.LoadState >= LoadState.Ready);
|
|
|
|
// If the new child has the flag set, we should propagate the flag towards the root.
|
|
// We can stop at the ancestor which has the flag already set because further ancestors will also have the flag set.
|
|
if (child.RequestsNonPositionalInputSubTree)
|
|
{
|
|
for (var ancestor = this; ancestor != null && !ancestor.RequestsNonPositionalInputSubTree; ancestor = ancestor.Parent)
|
|
ancestor.RequestsNonPositionalInputSubTree = true;
|
|
}
|
|
|
|
if (child.RequestsPositionalInputSubTree)
|
|
{
|
|
for (var ancestor = this; ancestor != null && !ancestor.RequestsPositionalInputSubTree; ancestor = ancestor.Parent)
|
|
ancestor.RequestsPositionalInputSubTree = true;
|
|
}
|
|
|
|
aliveInternalChildren.Add(child);
|
|
child.IsAlive = true;
|
|
|
|
ChildBecameAlive?.Invoke(child);
|
|
|
|
// Layout invalidations on non-alive children are blocked, so they must be invalidated once when they become alive.
|
|
child.Invalidate(Invalidation.Layout, InvalidationSource.Parent);
|
|
|
|
// Notify ourselves that a child has become alive.
|
|
Invalidate(Invalidation.Presence, InvalidationSource.Child);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Makes a child dead (not alive) and removes it if <see cref="Drawable.RemoveWhenNotAlive"/> of the <paramref name="child"/> is set.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Callers have to ensure that <paramref name="child"/> is of this <see cref="CompositeDrawable"/>'s <see cref="AliveInternalChildren"/>.
|
|
/// </remarks>
|
|
/// <param name="child">The child of this <see cref="CompositeDrawable"/>> to make dead.</param>
|
|
/// <returns>Whether <paramref name="child"/> has been removed by death.</returns>
|
|
protected bool MakeChildDead(Drawable child)
|
|
{
|
|
if (child.IsAlive)
|
|
{
|
|
aliveInternalChildren.Remove(child);
|
|
child.IsAlive = false;
|
|
|
|
ChildDied?.Invoke(child);
|
|
}
|
|
|
|
bool removed = false;
|
|
|
|
if (child.RemoveWhenNotAlive)
|
|
{
|
|
RemoveInternal(child, false);
|
|
|
|
if (child.DisposeOnDeathRemoval)
|
|
DisposeChildAsync(child);
|
|
|
|
removed = true;
|
|
}
|
|
|
|
// Notify ourselves that a child has died.
|
|
Invalidate(Invalidation.Presence, InvalidationSource.Child);
|
|
|
|
return removed;
|
|
}
|
|
|
|
internal override void UnbindAllBindablesSubTree()
|
|
{
|
|
base.UnbindAllBindablesSubTree();
|
|
|
|
// TODO: this code can potentially be run from an update thread while a drawable is still loading (see ScreenStack as an example).
|
|
// while this is quite a bad issue, it is rare and generally happens in tests which have frame perfect behaviours.
|
|
// as such, for loop is used here intentionally to avoid collection modified exceptions for this (usually) non-critical failure.
|
|
// see https://github.com/ppy/osu-framework/issues/4054.
|
|
for (int i = 0; i < internalChildren.Count; i++)
|
|
{
|
|
Drawable child = internalChildren[i];
|
|
child.UnbindAllBindablesSubTree();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unbinds a child's bindings synchronously and queues an asynchronous disposal of the child.
|
|
/// </summary>
|
|
/// <param name="drawable">The child to dispose.</param>
|
|
internal void DisposeChildAsync(Drawable drawable)
|
|
{
|
|
drawable.UnbindAllBindablesSubTree();
|
|
AsyncDisposalQueue.Enqueue(drawable);
|
|
}
|
|
|
|
internal override void UpdateClock(IFrameBasedClock clock)
|
|
{
|
|
if (Clock == clock)
|
|
return;
|
|
|
|
base.UpdateClock(clock);
|
|
foreach (Drawable child in internalChildren)
|
|
child.UpdateClock(Clock);
|
|
|
|
schedulerAfterChildren?.UpdateClock(Clock);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Specifies whether this <see cref="CompositeDrawable"/> requires an update of its children.
|
|
/// If the return value is false, then children are not updated and
|
|
/// <see cref="UpdateAfterChildren"/> is not called.
|
|
/// </summary>
|
|
protected virtual bool RequiresChildrenUpdate => !IsMaskedAway || !childrenSizeDependencies.IsValid;
|
|
|
|
public override bool UpdateSubTree()
|
|
{
|
|
if (!base.UpdateSubTree()) return false;
|
|
|
|
// We update our children's life even if we are invisible.
|
|
// Note, that this does not propagate down and may need
|
|
// generalization in the future.
|
|
UpdateChildrenLife();
|
|
|
|
// If we are not present then there is never a reason to check
|
|
// for children, as they should never affect our present status.
|
|
if (!IsPresent || !RequiresChildrenUpdate) return false;
|
|
|
|
UpdateAfterChildrenLife();
|
|
|
|
if (TypePerformanceMonitor.Active)
|
|
{
|
|
for (int i = 0; i < aliveInternalChildren.Count; ++i)
|
|
{
|
|
Drawable c = aliveInternalChildren[i];
|
|
|
|
TypePerformanceMonitor.BeginCollecting(c);
|
|
updateChild(c);
|
|
TypePerformanceMonitor.EndCollecting(c);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < aliveInternalChildren.Count; ++i)
|
|
updateChild(aliveInternalChildren[i]);
|
|
}
|
|
|
|
if (schedulerAfterChildren != null)
|
|
{
|
|
int amountScheduledTasks = schedulerAfterChildren.Update();
|
|
FrameStatistics.Add(StatisticsCounterType.ScheduleInvk, amountScheduledTasks);
|
|
}
|
|
|
|
UpdateAfterChildren();
|
|
|
|
updateChildrenSizeDependencies();
|
|
UpdateAfterAutoSize();
|
|
return true;
|
|
}
|
|
|
|
private void updateChild(Drawable c)
|
|
{
|
|
Debug.Assert(c.LoadState >= LoadState.Ready);
|
|
c.UpdateSubTree();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates all masking calculations for this <see cref="CompositeDrawable"/> and its <see cref="AliveInternalChildren"/>.
|
|
/// This occurs post-<see cref="UpdateSubTree"/> to ensure that all <see cref="Drawable"/> updates have taken place.
|
|
/// </summary>
|
|
/// <param name="source">The parent that triggered this update on this <see cref="Drawable"/>.</param>
|
|
/// <param name="maskingBounds">The <see cref="RectangleF"/> that defines the masking bounds.</param>
|
|
/// <returns>Whether masking calculations have taken place.</returns>
|
|
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds)
|
|
{
|
|
if (!base.UpdateSubTreeMasking(source, maskingBounds))
|
|
return false;
|
|
|
|
if (IsMaskedAway)
|
|
return true;
|
|
|
|
if (aliveInternalChildren.Count == 0)
|
|
return true;
|
|
|
|
if (RequiresChildrenUpdate)
|
|
{
|
|
var childMaskingBounds = ComputeChildMaskingBounds(maskingBounds);
|
|
|
|
for (int i = 0; i < aliveInternalChildren.Count; i++)
|
|
aliveInternalChildren[i].UpdateSubTreeMasking(this, childMaskingBounds);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds)
|
|
{
|
|
if (!CanBeFlattened)
|
|
return base.ComputeIsMaskedAway(maskingBounds);
|
|
|
|
// The masking check is overly expensive (requires creation of ScreenSpaceDrawQuad)
|
|
// when only few children exist.
|
|
return aliveInternalChildren.Count >= amount_children_required_for_masking_check && base.ComputeIsMaskedAway(maskingBounds);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the <see cref="RectangleF"/> to be used as the masking bounds for all <see cref="AliveInternalChildren"/>.
|
|
/// </summary>
|
|
/// <param name="maskingBounds">The <see cref="RectangleF"/> that defines the masking bounds for this <see cref="CompositeDrawable"/>.</param>
|
|
/// <returns>The <see cref="RectangleF"/> to be used as the masking bounds for <see cref="AliveInternalChildren"/>.</returns>
|
|
protected virtual RectangleF ComputeChildMaskingBounds(RectangleF maskingBounds) => Masking ? RectangleF.Intersect(maskingBounds, ScreenSpaceDrawQuad.AABBFloat) : maskingBounds;
|
|
|
|
/// <summary>
|
|
/// Invoked after <see cref="UpdateChildrenLife"/> and <see cref="Drawable.IsPresent"/> state checks have taken place,
|
|
/// but before <see cref="Drawable.UpdateSubTree"/> is invoked for all <see cref="InternalChildren"/>.
|
|
/// This occurs after <see cref="Drawable.Update"/> has been invoked on this <see cref="CompositeDrawable"/>
|
|
/// </summary>
|
|
protected virtual void UpdateAfterChildrenLife()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// An opportunity to update state once-per-frame after <see cref="Drawable.Update"/> has been called
|
|
/// for all <see cref="InternalChildren"/>.
|
|
/// This is invoked prior to any autosize calculations of this <see cref="CompositeDrawable"/>.
|
|
/// </summary>
|
|
protected virtual void UpdateAfterChildren()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked after all autosize calculations have taken place.
|
|
/// </summary>
|
|
protected virtual void UpdateAfterAutoSize()
|
|
{
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Invalidation
|
|
|
|
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
|
|
{
|
|
bool anyInvalidated = base.OnInvalidate(invalidation, source);
|
|
|
|
// Child invalidations should not propagate to other children.
|
|
if (source == InvalidationSource.Child)
|
|
return anyInvalidated;
|
|
|
|
// DrawNode invalidations should not propagate to children.
|
|
invalidation &= ~Invalidation.DrawNode;
|
|
if (invalidation == Invalidation.None)
|
|
return anyInvalidated;
|
|
|
|
IReadOnlyList<Drawable> targetChildren = aliveInternalChildren;
|
|
|
|
// Non-layout flags must be propagated to all children. As such, it is simplest + quickest to propagate all other relevant flags along with them.
|
|
if ((invalidation & ~Invalidation.Layout) > 0)
|
|
targetChildren = internalChildren;
|
|
|
|
for (int i = 0; i < targetChildren.Count; ++i)
|
|
{
|
|
Drawable c = targetChildren[i];
|
|
|
|
Invalidation childInvalidation = invalidation;
|
|
if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0)
|
|
childInvalidation |= Invalidation.DrawInfo;
|
|
|
|
// Other geometry things like rotation, shearing, etc don't affect child properties.
|
|
childInvalidation &= ~Invalidation.MiscGeometry;
|
|
|
|
// Relative positioning can however affect child geometry.
|
|
if (c.RelativePositionAxes != Axes.None && (invalidation & Invalidation.DrawSize) > 0)
|
|
childInvalidation |= Invalidation.MiscGeometry;
|
|
|
|
// No draw size changes if relative size axes does not propagate it downward.
|
|
if (c.RelativeSizeAxes == Axes.None)
|
|
childInvalidation &= ~Invalidation.DrawSize;
|
|
|
|
anyInvalidated |= c.Invalidate(childInvalidation, InvalidationSource.Parent);
|
|
}
|
|
|
|
return anyInvalidated;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invalidates the children size dependencies of this <see cref="CompositeDrawable"/> when a child's position or size changes.
|
|
/// </summary>
|
|
/// <param name="invalidation">The <see cref="Invalidation"/> to invalidate with.</param>
|
|
/// <param name="axes">The position or size <see cref="Axes"/> that changed.</param>
|
|
/// <param name="source">The source <see cref="Drawable"/>.</param>
|
|
internal void InvalidateChildrenSizeDependencies(Invalidation invalidation, Axes axes, Drawable source)
|
|
{
|
|
// Store the current state of the children size dependencies.
|
|
// This state may be restored later if the invalidation proved to be unnecessary.
|
|
bool wasValid = childrenSizeDependencies.IsValid;
|
|
|
|
// The invalidation still needs to occur as normal, since a derived CompositeDrawable may want to respond to children size invalidations.
|
|
Invalidate(invalidation, InvalidationSource.Child);
|
|
|
|
// Skip axes that are bypassed.
|
|
axes &= ~source.BypassAutoSizeAxes;
|
|
|
|
// Include only axes that this composite is autosizing for.
|
|
axes &= AutoSizeAxes;
|
|
|
|
// If no remaining axes remain, then children size dependencies can immediately be re-validated as the auto-sized size would not change.
|
|
if (wasValid && axes == Axes.None)
|
|
childrenSizeDependencies.Validate();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region DrawNode
|
|
|
|
private bool hasCustomDrawNode;
|
|
|
|
internal IShader Shader { get; private set; }
|
|
|
|
protected override DrawNode CreateDrawNode() => new CompositeDrawableDrawNode(this);
|
|
|
|
private bool forceLocalVertexBatch;
|
|
|
|
/// <summary>
|
|
/// Whether to use a local vertex batch for rendering. If false, a parenting vertex batch will be used.
|
|
/// </summary>
|
|
public bool ForceLocalVertexBatch
|
|
{
|
|
get => forceLocalVertexBatch;
|
|
protected set
|
|
{
|
|
if (forceLocalVertexBatch == value)
|
|
return;
|
|
|
|
forceLocalVertexBatch = value;
|
|
|
|
Invalidate(Invalidation.DrawNode);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A flattened <see cref="CompositeDrawable"/> has its <see cref="DrawNode"/> merged into its parents'.
|
|
/// In some cases, the <see cref="DrawNode"/> must always be generated and flattening should not occur.
|
|
/// </summary>
|
|
protected virtual bool CanBeFlattened =>
|
|
// Masking composite draw nodes define the masking area for their children.
|
|
!Masking
|
|
// Proxied drawables have their DrawNodes drawn elsewhere in the scene graph.
|
|
&& !HasProxy
|
|
// Custom draw nodes may provide custom drawing procedures.
|
|
&& !hasCustomDrawNode
|
|
// Composites with local vertex batches require their own draw node.
|
|
&& !ForceLocalVertexBatch;
|
|
|
|
private const int amount_children_required_for_masking_check = 2;
|
|
|
|
/// <summary>
|
|
/// This function adds all children's <see cref="DrawNode"/>s to a target List, flattening the children of certain types
|
|
/// of <see cref="CompositeDrawable"/> subtrees for optimization purposes.
|
|
/// </summary>
|
|
/// <param name="frame">The frame which <see cref="DrawNode"/>s should be generated for.</param>
|
|
/// <param name="treeIndex">The index of the currently in-use <see cref="DrawNode"/> tree.</param>
|
|
/// <param name="forceNewDrawNode">Whether the creation of a new <see cref="DrawNode"/> should be forced, rather than re-using an existing <see cref="DrawNode"/>.</param>
|
|
/// <param name="j">The running index into the target List.</param>
|
|
/// <param name="parentComposite">The <see cref="CompositeDrawable"/> whose children's <see cref="DrawNode"/>s to add.</param>
|
|
/// <param name="target">The target list to fill with DrawNodes.</param>
|
|
private static void addFromComposite(ulong frame, int treeIndex, bool forceNewDrawNode, ref int j, CompositeDrawable parentComposite, List<DrawNode> target)
|
|
{
|
|
SortedList<Drawable> children = parentComposite.aliveInternalChildren;
|
|
|
|
for (int i = 0; i < children.Count; ++i)
|
|
{
|
|
Drawable drawable = children[i];
|
|
|
|
if (!drawable.IsLoaded)
|
|
continue;
|
|
|
|
if (!drawable.IsProxy)
|
|
{
|
|
if (!drawable.IsPresent)
|
|
continue;
|
|
|
|
if (drawable.IsMaskedAway)
|
|
continue;
|
|
|
|
CompositeDrawable composite = drawable as CompositeDrawable;
|
|
|
|
if (composite?.CanBeFlattened == true)
|
|
{
|
|
addFromComposite(frame, treeIndex, forceNewDrawNode, ref j, composite, target);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
DrawNode next = drawable.GenerateDrawNodeSubtree(frame, treeIndex, forceNewDrawNode);
|
|
if (next == null)
|
|
continue;
|
|
|
|
if (drawable.HasProxy)
|
|
drawable.ValidateProxyDrawNode(treeIndex, frame);
|
|
else
|
|
{
|
|
if (j < target.Count)
|
|
target[j] = next;
|
|
else
|
|
target.Add(next);
|
|
j++;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal override DrawNode GenerateDrawNodeSubtree(ulong frame, int treeIndex, bool forceNewDrawNode)
|
|
{
|
|
// No need for a draw node at all if there are no children and we are not glowing.
|
|
if (aliveInternalChildren.Count == 0 && CanBeFlattened)
|
|
return null;
|
|
|
|
DrawNode node = base.GenerateDrawNodeSubtree(frame, treeIndex, forceNewDrawNode);
|
|
|
|
if (!(node is ICompositeDrawNode cNode))
|
|
return null;
|
|
|
|
cNode.Children ??= new List<DrawNode>(aliveInternalChildren.Count);
|
|
|
|
if (cNode.AddChildDrawNodes)
|
|
{
|
|
int j = 0;
|
|
addFromComposite(frame, treeIndex, forceNewDrawNode, ref j, this, cNode.Children);
|
|
|
|
if (j < cNode.Children.Count)
|
|
cNode.Children.RemoveRange(j, cNode.Children.Count - j);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Transforms
|
|
|
|
/// <summary>
|
|
/// Whether to remove completed transforms from the list of applicable transforms. Setting this to false allows for rewinding transforms.
|
|
/// <para>
|
|
/// This value is passed down to children.
|
|
/// </para>
|
|
/// </summary>
|
|
public override bool RemoveCompletedTransforms
|
|
{
|
|
get => base.RemoveCompletedTransforms;
|
|
internal set
|
|
{
|
|
if (base.RemoveCompletedTransforms == value)
|
|
return;
|
|
|
|
base.RemoveCompletedTransforms = value;
|
|
|
|
foreach (var c in internalChildren)
|
|
c.RemoveCompletedTransforms = RemoveCompletedTransforms;
|
|
}
|
|
}
|
|
|
|
public override void ApplyTransformsAt(double time, bool propagateChildren = false)
|
|
{
|
|
EnsureTransformMutationAllowed();
|
|
|
|
base.ApplyTransformsAt(time, propagateChildren);
|
|
|
|
if (!propagateChildren)
|
|
return;
|
|
|
|
foreach (var c in internalChildren)
|
|
c.ApplyTransformsAt(time, true);
|
|
}
|
|
|
|
public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null)
|
|
{
|
|
EnsureTransformMutationAllowed();
|
|
|
|
base.ClearTransformsAfter(time, propagateChildren, targetMember);
|
|
|
|
if (AutoSizeAxes != Axes.None && AutoSizeDuration > 0)
|
|
childrenSizeDependencies.Invalidate();
|
|
|
|
if (!propagateChildren)
|
|
return;
|
|
|
|
foreach (var c in internalChildren)
|
|
c.ClearTransformsAfter(time, true, targetMember);
|
|
}
|
|
|
|
internal override void AddDelay(double duration, bool propagateChildren = false)
|
|
{
|
|
if (duration == 0)
|
|
return;
|
|
|
|
base.AddDelay(duration, propagateChildren);
|
|
|
|
if (propagateChildren)
|
|
{
|
|
foreach (var c in internalChildren)
|
|
c.AddDelay(duration, true);
|
|
}
|
|
}
|
|
|
|
protected internal ScheduledDelegate ScheduleAfterChildren<T>(Action<T> action, T data)
|
|
{
|
|
if (TransformDelay > 0)
|
|
return SchedulerAfterChildren.AddDelayed(action, data, TransformDelay);
|
|
|
|
return SchedulerAfterChildren.Add(action, data);
|
|
}
|
|
|
|
protected internal ScheduledDelegate ScheduleAfterChildren(Action action)
|
|
{
|
|
if (TransformDelay > 0)
|
|
return SchedulerAfterChildren.AddDelayed(action, TransformDelay);
|
|
|
|
return SchedulerAfterChildren.Add(action);
|
|
}
|
|
|
|
public override IDisposable BeginAbsoluteSequence(double newTransformStartTime, bool recursive = true)
|
|
{
|
|
EnsureTransformMutationAllowed();
|
|
|
|
if (!recursive || internalChildren.Count == 0)
|
|
return base.BeginAbsoluteSequence(newTransformStartTime, false);
|
|
|
|
List<AbsoluteSequenceSender> disposalActions = new List<AbsoluteSequenceSender>(internalChildren.Count + 1);
|
|
|
|
base.CollectAbsoluteSequenceActionsFromSubTree(newTransformStartTime, disposalActions);
|
|
|
|
foreach (var c in internalChildren)
|
|
c.CollectAbsoluteSequenceActionsFromSubTree(newTransformStartTime, disposalActions);
|
|
|
|
return new ValueInvokeOnDisposal<List<AbsoluteSequenceSender>>(disposalActions, actions =>
|
|
{
|
|
foreach (var a in actions)
|
|
a.Dispose();
|
|
});
|
|
}
|
|
|
|
internal override void CollectAbsoluteSequenceActionsFromSubTree(double newTransformStartTime, List<AbsoluteSequenceSender> actions)
|
|
{
|
|
base.CollectAbsoluteSequenceActionsFromSubTree(newTransformStartTime, actions);
|
|
|
|
foreach (var c in internalChildren)
|
|
c.CollectAbsoluteSequenceActionsFromSubTree(newTransformStartTime, actions);
|
|
}
|
|
|
|
public override void FinishTransforms(bool propagateChildren = false, string targetMember = null)
|
|
{
|
|
EnsureTransformMutationAllowed();
|
|
|
|
base.FinishTransforms(propagateChildren, targetMember);
|
|
|
|
if (propagateChildren)
|
|
{
|
|
// Use for over foreach as collection may grow due to abort / completion events.
|
|
// Note that this may mean that in the addition of elements being removed,
|
|
// `FinishTransforms` may not be called on all items.
|
|
for (int i = 0; i < internalChildren.Count; i++)
|
|
internalChildren[i].FinishTransforms(true, targetMember);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper function for creating and adding a <see cref="Transform{TValue, T}"/> that fades the current <see cref="EdgeEffect"/>.
|
|
/// </summary>
|
|
protected TransformSequence<CompositeDrawable> FadeEdgeEffectTo(float newAlpha, double duration = 0, Easing easing = Easing.None)
|
|
{
|
|
Color4 targetColour = EdgeEffect.Colour;
|
|
targetColour.A = newAlpha;
|
|
return FadeEdgeEffectTo(targetColour, duration, easing);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper function for creating and adding a <see cref="Transform{TValue, T}"/> that fades the current <see cref="EdgeEffect"/>.
|
|
/// </summary>
|
|
protected TransformSequence<CompositeDrawable> FadeEdgeEffectTo(Color4 newColour, double duration = 0, Easing easing = Easing.None)
|
|
{
|
|
var effect = EdgeEffect;
|
|
effect.Colour = newColour;
|
|
return TweenEdgeEffectTo(effect, duration, easing);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper function for creating and adding a <see cref="Transform{TValue, T}"/> that tweens the current <see cref="EdgeEffect"/>.
|
|
/// </summary>
|
|
protected TransformSequence<CompositeDrawable> TweenEdgeEffectTo(EdgeEffectParameters newParams, double duration = 0, Easing easing = Easing.None) =>
|
|
this.TransformTo(nameof(EdgeEffect), newParams, duration, easing);
|
|
|
|
internal void EnsureChildMutationAllowed() => EnsureMutationAllowed($"mutate the {nameof(InternalChildren)}");
|
|
|
|
#endregion
|
|
|
|
#region Interaction / Input
|
|
|
|
public override bool Contains(Vector2 screenSpacePos)
|
|
{
|
|
float cRadius = effectiveCornerRadius;
|
|
float cExponent = CornerExponent;
|
|
|
|
// Select a cheaper contains method when we don't need rounded edges.
|
|
if (cRadius == 0.0f)
|
|
return base.Contains(screenSpacePos);
|
|
|
|
return DrawRectangle.Shrink(cRadius).DistanceExponentiated(ToLocalSpace(screenSpacePos), cExponent) <= Math.Pow(cRadius, cExponent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check whether a child should be considered for inclusion in <see cref="BuildNonPositionalInputQueue"/> and <see cref="BuildPositionalInputQueue"/>
|
|
/// </summary>
|
|
/// <param name="child">The drawable to be evaluated.</param>
|
|
/// <returns>Whether or not the specified drawable should be considered when building input queues.</returns>
|
|
protected virtual bool ShouldBeConsideredForInput(Drawable child) => child.LoadState == LoadState.Loaded;
|
|
|
|
internal override bool BuildNonPositionalInputQueue(List<Drawable> queue, bool allowBlocking = true)
|
|
{
|
|
if (!base.BuildNonPositionalInputQueue(queue, allowBlocking))
|
|
return false;
|
|
|
|
for (int i = 0; i < aliveInternalChildren.Count; ++i)
|
|
{
|
|
if (ShouldBeConsideredForInput(aliveInternalChildren[i]))
|
|
aliveInternalChildren[i].BuildNonPositionalInputQueue(queue, allowBlocking);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the subtree of this <see cref="CompositeDrawable"/> should receive positional input when the mouse is at the given screen-space position.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// By default, the subtree of this <see cref="CompositeDrawable"/> always receives input when masking is turned off, and only receives input if this
|
|
/// <see cref="CompositeDrawable"/> also receives input when masking is turned on.
|
|
/// </remarks>
|
|
/// <param name="screenSpacePos">The screen-space position where input could be received.</param>
|
|
/// <returns>True if the subtree should receive input at the given screen-space position.</returns>
|
|
protected virtual bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => !Masking || ReceivePositionalInputAt(screenSpacePos);
|
|
|
|
internal override bool BuildPositionalInputQueue(Vector2 screenSpacePos, List<Drawable> queue)
|
|
{
|
|
if (!base.BuildPositionalInputQueue(screenSpacePos, queue))
|
|
return false;
|
|
|
|
if (!ReceivePositionalInputAtSubTree(screenSpacePos))
|
|
return false;
|
|
|
|
for (int i = 0; i < aliveInternalChildren.Count; ++i)
|
|
{
|
|
if (ShouldBeConsideredForInput(aliveInternalChildren[i]))
|
|
aliveInternalChildren[i].BuildPositionalInputQueue(screenSpacePos, queue);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Masking and related effects (e.g. round corners)
|
|
|
|
private bool masking;
|
|
|
|
/// <summary>
|
|
/// If enabled, only the portion of children that falls within this <see cref="CompositeDrawable"/>'s
|
|
/// shape is drawn to the screen.
|
|
/// </summary>
|
|
public bool Masking
|
|
{
|
|
get => masking;
|
|
protected set
|
|
{
|
|
if (masking == value)
|
|
return;
|
|
|
|
masking = value;
|
|
Invalidate(Invalidation.DrawNode);
|
|
}
|
|
}
|
|
|
|
private float maskingSmoothness = 1;
|
|
|
|
/// <summary>
|
|
/// Determines over how many pixels the alpha component smoothly fades out.
|
|
/// Only has an effect when <see cref="Masking"/> is true.
|
|
/// </summary>
|
|
public float MaskingSmoothness
|
|
{
|
|
get => maskingSmoothness;
|
|
protected set
|
|
{
|
|
//must be above zero to avoid a div-by-zero in the shader logic.
|
|
value = Math.Max(0.01f, value);
|
|
|
|
if (maskingSmoothness == value)
|
|
return;
|
|
|
|
maskingSmoothness = value;
|
|
Invalidate(Invalidation.DrawNode);
|
|
}
|
|
}
|
|
|
|
private float cornerRadius;
|
|
|
|
/// <summary>
|
|
/// Determines how large of a radius is masked away around the corners.
|
|
/// Only has an effect when <see cref="Masking"/> is true.
|
|
/// </summary>
|
|
public float CornerRadius
|
|
{
|
|
get => cornerRadius;
|
|
protected set
|
|
{
|
|
if (cornerRadius == value)
|
|
return;
|
|
|
|
cornerRadius = value;
|
|
Invalidate(Invalidation.DrawNode);
|
|
}
|
|
}
|
|
|
|
private float cornerExponent = 2f;
|
|
|
|
/// <summary>
|
|
/// Determines how gentle the curve of the corner straightens. A value of 2 (default) results in
|
|
/// circular arcs, a value of 2.5 results in something closer to apple's "continuous corner".
|
|
/// Values between 2 and 10 result in varying degrees of "continuousness", where larger values are smoother.
|
|
/// Values between 1 and 2 result in a "flatter" appearance than round corners.
|
|
/// Values between 0 and 1 result in a concave, round corner as opposed to a convex round corner,
|
|
/// where a value of 0.5 is a circular concave arc.
|
|
/// Only has an effect when <see cref="Masking"/> is true and <see cref="CornerRadius"/> is non-zero.
|
|
/// </summary>
|
|
public float CornerExponent
|
|
{
|
|
get => cornerExponent;
|
|
protected set
|
|
{
|
|
if (!Precision.DefinitelyBigger(value, 0) || value > 10)
|
|
throw new ArgumentOutOfRangeException(nameof(CornerExponent), $"{nameof(CornerExponent)} may not be <=0 or >10 for numerical correctness.");
|
|
|
|
if (cornerExponent == value)
|
|
return;
|
|
|
|
cornerExponent = value;
|
|
Invalidate(Invalidation.DrawNode);
|
|
}
|
|
}
|
|
|
|
// This _hacky_ modification of the corner radius (obtained from playing around) ensures that the corner remains at roughly
|
|
// equal size (perceptually) compared to the circular arc as the CornerExponent is adjusted within the range ~2-5.
|
|
private float effectiveCornerRadius => CornerRadius * 0.8f * CornerExponent / 2 + 0.2f * CornerRadius;
|
|
|
|
private float borderThickness;
|
|
|
|
/// <summary>
|
|
/// Determines how thick of a border to draw around the inside of the masked region.
|
|
/// Only has an effect when <see cref="Masking"/> is true.
|
|
/// The border only is drawn on top of children using a sprite shader.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Drawing borders is optimized heavily into our sprite shaders. As a consequence
|
|
/// borders are only drawn correctly on top of quad-shaped children using our sprite
|
|
/// shaders.
|
|
/// </remarks>
|
|
public float BorderThickness
|
|
{
|
|
get => borderThickness;
|
|
protected set
|
|
{
|
|
if (borderThickness == value)
|
|
return;
|
|
|
|
borderThickness = value;
|
|
Invalidate(Invalidation.DrawNode);
|
|
}
|
|
}
|
|
|
|
private ColourInfo borderColour = Color4.Black;
|
|
|
|
/// <summary>
|
|
/// Determines the color of the border controlled by <see cref="BorderThickness"/>.
|
|
/// Only has an effect when <see cref="Masking"/> is true.
|
|
/// </summary>
|
|
public ColourInfo BorderColour
|
|
{
|
|
get => borderColour;
|
|
protected set
|
|
{
|
|
if (borderColour.Equals(value))
|
|
return;
|
|
|
|
borderColour = value;
|
|
Invalidate(Invalidation.DrawNode);
|
|
}
|
|
}
|
|
|
|
private EdgeEffectParameters edgeEffect;
|
|
|
|
/// <summary>
|
|
/// Determines an edge effect of this <see cref="CompositeDrawable"/>.
|
|
/// Edge effects are e.g. glow or a shadow.
|
|
/// Only has an effect when <see cref="Masking"/> is true.
|
|
/// </summary>
|
|
public EdgeEffectParameters EdgeEffect
|
|
{
|
|
get => edgeEffect;
|
|
protected set
|
|
{
|
|
if (edgeEffect.Equals(value))
|
|
return;
|
|
|
|
edgeEffect = value;
|
|
Invalidate(Invalidation.DrawNode);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Sizing
|
|
|
|
public override RectangleF BoundingBox
|
|
{
|
|
get
|
|
{
|
|
float cRadius = CornerRadius;
|
|
if (cRadius == 0.0f)
|
|
return base.BoundingBox;
|
|
|
|
RectangleF drawRect = LayoutRectangle.Shrink(cRadius);
|
|
|
|
// Inflate bounding box in parent space by the half-size of the bounding box of the
|
|
// ellipse obtained by transforming the unit circle into parent space.
|
|
Vector2 offset = ToParentSpace(Vector2.Zero);
|
|
Vector2 u = ToParentSpace(new Vector2(cRadius, 0)) - offset;
|
|
Vector2 v = ToParentSpace(new Vector2(0, cRadius)) - offset;
|
|
Vector2 inflation = new Vector2(
|
|
MathF.Sqrt(u.X * u.X + v.X * v.X),
|
|
MathF.Sqrt(u.Y * u.Y + v.Y * v.Y)
|
|
);
|
|
|
|
RectangleF result = ToParentSpace(drawRect).AABBFloat.Inflate(inflation);
|
|
// The above algorithm will return incorrect results if the rounded corners are not fully visible.
|
|
// To limit bad behavior we at least enforce here, that the bounding box with rounded corners
|
|
// is never larger than the bounding box without.
|
|
if (DrawSize.X < CornerRadius * 2 || DrawSize.Y < CornerRadius * 2)
|
|
result.Intersect(base.BoundingBox);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private MarginPadding padding;
|
|
|
|
/// <summary>
|
|
/// Shrinks the space children may occupy within this <see cref="CompositeDrawable"/>
|
|
/// by the specified amount on each side.
|
|
/// </summary>
|
|
public MarginPadding Padding
|
|
{
|
|
get => padding;
|
|
protected set
|
|
{
|
|
if (padding.Equals(value)) return;
|
|
|
|
if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Padding)} must be finite, but is {value}.");
|
|
|
|
padding = value;
|
|
|
|
foreach (Drawable c in internalChildren)
|
|
c.Invalidate(c.InvalidationFromParentSize | Invalidation.MiscGeometry);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The size of the coordinate space revealed to <see cref="InternalChildren"/>.
|
|
/// Captures the effect of e.g. <see cref="Padding"/>.
|
|
/// </summary>
|
|
public Vector2 ChildSize => DrawSize - new Vector2(Padding.TotalHorizontal, Padding.TotalVertical);
|
|
|
|
/// <summary>
|
|
/// Positional offset applied to <see cref="InternalChildren"/>.
|
|
/// Captures the effect of e.g. <see cref="Padding"/>.
|
|
/// </summary>
|
|
public Vector2 ChildOffset => new Vector2(Padding.Left, Padding.Top);
|
|
|
|
private Vector2 relativeChildSize = Vector2.One;
|
|
|
|
/// <summary>
|
|
/// The size of the relative position/size coordinate space of children of this <see cref="CompositeDrawable"/>.
|
|
/// Children positioned at this size will appear as if they were positioned at <see cref="Drawable.Position"/> = <see cref="Vector2.One"/> in this <see cref="CompositeDrawable"/>.
|
|
/// </summary>
|
|
public Vector2 RelativeChildSize
|
|
{
|
|
get => relativeChildSize;
|
|
protected set
|
|
{
|
|
if (relativeChildSize == value)
|
|
return;
|
|
|
|
if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(RelativeChildSize)} must be finite, but is {value}.");
|
|
if (value.X == 0 || value.Y == 0) throw new ArgumentException($@"{nameof(RelativeChildSize)} must be non-zero, but is {value}.");
|
|
|
|
relativeChildSize = value;
|
|
|
|
foreach (Drawable c in internalChildren)
|
|
c.Invalidate(c.InvalidationFromParentSize);
|
|
}
|
|
}
|
|
|
|
private Vector2 relativeChildOffset = Vector2.Zero;
|
|
|
|
/// <summary>
|
|
/// The offset of the relative position/size coordinate space of children of this <see cref="CompositeDrawable"/>.
|
|
/// Children positioned at this offset will appear as if they were positioned at <see cref="Drawable.Position"/> = <see cref="Vector2.Zero"/> in this <see cref="CompositeDrawable"/>.
|
|
/// </summary>
|
|
public Vector2 RelativeChildOffset
|
|
{
|
|
get => relativeChildOffset;
|
|
protected set
|
|
{
|
|
if (relativeChildOffset == value)
|
|
return;
|
|
|
|
if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(RelativeChildOffset)} must be finite, but is {value}.");
|
|
|
|
relativeChildOffset = value;
|
|
|
|
foreach (Drawable c in internalChildren)
|
|
c.Invalidate(c.InvalidationFromParentSize & ~Invalidation.DrawSize);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Conversion factor from relative to absolute coordinates in our space.
|
|
/// </summary>
|
|
public Vector2 RelativeToAbsoluteFactor => Vector2.Divide(ChildSize, RelativeChildSize);
|
|
|
|
/// <summary>
|
|
/// Tweens the <see cref="RelativeChildSize"/> of this <see cref="CompositeDrawable"/>.
|
|
/// </summary>
|
|
/// <param name="newSize">The coordinate space to tween to.</param>
|
|
/// <param name="duration">The tween duration.</param>
|
|
/// <param name="easing">The tween easing.</param>
|
|
protected TransformSequence<CompositeDrawable> TransformRelativeChildSizeTo(Vector2 newSize, double duration = 0, Easing easing = Easing.None)
|
|
{
|
|
if (newSize.X == 0 || newSize.Y == 0)
|
|
throw new ArgumentException($@"{nameof(newSize)} must be non-zero, but is {newSize}.", nameof(newSize));
|
|
|
|
return this.TransformTo(nameof(RelativeChildSize), newSize, duration, easing);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tweens the <see cref="RelativeChildOffset"/> of this <see cref="CompositeDrawable"/>.
|
|
/// </summary>
|
|
/// <param name="newOffset">The coordinate space to tween to.</param>
|
|
/// <param name="duration">The tween duration.</param>
|
|
/// <param name="easing">The tween easing.</param>
|
|
protected TransformSequence<CompositeDrawable> TransformRelativeChildOffsetTo(Vector2 newOffset, double duration = 0, Easing easing = Easing.None) =>
|
|
this.TransformTo(nameof(RelativeChildOffset), newOffset, duration, easing);
|
|
|
|
public override Axes RelativeSizeAxes
|
|
{
|
|
get => base.RelativeSizeAxes;
|
|
set
|
|
{
|
|
if ((AutoSizeAxes & value) != 0)
|
|
throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time.");
|
|
|
|
base.RelativeSizeAxes = value;
|
|
}
|
|
}
|
|
|
|
private Axes autoSizeAxes;
|
|
|
|
/// <summary>
|
|
/// Controls which <see cref="Axes"/> are automatically sized w.r.t. <see cref="InternalChildren"/>.
|
|
/// Children's <see cref="Drawable.BypassAutoSizeAxes"/> are ignored for automatic sizing.
|
|
/// Most notably, <see cref="Drawable.RelativePositionAxes"/> and <see cref="RelativeSizeAxes"/> of children
|
|
/// do not affect automatic sizing to avoid circular size dependencies.
|
|
/// It is not allowed to manually set <see cref="Size"/> (or <see cref="Width"/> / <see cref="Height"/>)
|
|
/// on any <see cref="Axes"/> which are automatically sized.
|
|
/// </summary>
|
|
public virtual Axes AutoSizeAxes
|
|
{
|
|
get => autoSizeAxes;
|
|
protected set
|
|
{
|
|
if (value == autoSizeAxes)
|
|
return;
|
|
|
|
if ((RelativeSizeAxes & value) != 0)
|
|
throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time.");
|
|
|
|
autoSizeAxes = value;
|
|
|
|
if (value == Axes.None)
|
|
childrenSizeDependencies.Validate();
|
|
else
|
|
childrenSizeDependencies.Invalidate();
|
|
|
|
OnSizingChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The duration which automatic sizing should take. If zero, then it is instantaneous.
|
|
/// Otherwise, this is equivalent to applying an automatic size via a resize transform.
|
|
/// </summary>
|
|
public float AutoSizeDuration { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// The type of easing which should be used for smooth automatic sizing when <see cref="AutoSizeDuration"/>
|
|
/// is non-zero.
|
|
/// </summary>
|
|
public Easing AutoSizeEasing { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// Fired after this <see cref="CompositeDrawable"/>'s <see cref="Size"/> is updated through autosize.
|
|
/// </summary>
|
|
internal event Action OnAutoSize;
|
|
|
|
/// <summary>
|
|
/// Whether the <see cref="childrenSizeDependencies"/> layout is valid.
|
|
/// </summary>
|
|
internal bool ChildrenSizeDependenciesIsValid => childrenSizeDependencies.IsValid;
|
|
|
|
private readonly LayoutValue childrenSizeDependencies = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.Presence, InvalidationSource.Child);
|
|
|
|
public override float Width
|
|
{
|
|
get
|
|
{
|
|
if (!isComputingChildrenSizeDependencies && AutoSizeAxes.HasFlagFast(Axes.X))
|
|
updateChildrenSizeDependencies();
|
|
return base.Width;
|
|
}
|
|
|
|
set
|
|
{
|
|
if ((AutoSizeAxes & Axes.X) != 0)
|
|
throw new InvalidOperationException($"The width of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} can not be set manually.");
|
|
|
|
base.Width = value;
|
|
}
|
|
}
|
|
|
|
public override float Height
|
|
{
|
|
get
|
|
{
|
|
if (!isComputingChildrenSizeDependencies && AutoSizeAxes.HasFlagFast(Axes.Y))
|
|
updateChildrenSizeDependencies();
|
|
return base.Height;
|
|
}
|
|
|
|
set
|
|
{
|
|
if ((AutoSizeAxes & Axes.Y) != 0)
|
|
throw new InvalidOperationException($"The height of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} can not be set manually.");
|
|
|
|
base.Height = value;
|
|
}
|
|
}
|
|
|
|
private bool isComputingChildrenSizeDependencies;
|
|
|
|
public override Vector2 Size
|
|
{
|
|
get
|
|
{
|
|
if (!isComputingChildrenSizeDependencies && AutoSizeAxes != Axes.None)
|
|
updateChildrenSizeDependencies();
|
|
return base.Size;
|
|
}
|
|
|
|
set
|
|
{
|
|
if ((AutoSizeAxes & Axes.Both) != 0)
|
|
throw new InvalidOperationException($"The Size of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} can not be set manually.");
|
|
|
|
base.Size = value;
|
|
}
|
|
}
|
|
|
|
private Vector2 computeAutoSize()
|
|
{
|
|
MarginPadding originalPadding = Padding;
|
|
MarginPadding originalMargin = Margin;
|
|
|
|
try
|
|
{
|
|
Padding = new MarginPadding();
|
|
Margin = new MarginPadding();
|
|
|
|
if (AutoSizeAxes == Axes.None) return DrawSize;
|
|
|
|
Vector2 maxBoundSize = Vector2.Zero;
|
|
|
|
// Find the maximum width/height of children
|
|
foreach (Drawable c in aliveInternalChildren)
|
|
{
|
|
if (!c.IsPresent)
|
|
continue;
|
|
|
|
Vector2 cBound = c.RequiredParentSizeToFit;
|
|
|
|
if (!c.BypassAutoSizeAxes.HasFlagFast(Axes.X))
|
|
maxBoundSize.X = Math.Max(maxBoundSize.X, cBound.X);
|
|
|
|
if (!c.BypassAutoSizeAxes.HasFlagFast(Axes.Y))
|
|
maxBoundSize.Y = Math.Max(maxBoundSize.Y, cBound.Y);
|
|
}
|
|
|
|
if (!AutoSizeAxes.HasFlagFast(Axes.X))
|
|
maxBoundSize.X = DrawSize.X;
|
|
if (!AutoSizeAxes.HasFlagFast(Axes.Y))
|
|
maxBoundSize.Y = DrawSize.Y;
|
|
|
|
return new Vector2(maxBoundSize.X, maxBoundSize.Y);
|
|
}
|
|
finally
|
|
{
|
|
Padding = originalPadding;
|
|
Margin = originalMargin;
|
|
}
|
|
}
|
|
|
|
private void updateAutoSize()
|
|
{
|
|
if (AutoSizeAxes == Axes.None)
|
|
return;
|
|
|
|
Vector2 b = computeAutoSize() + Padding.Total;
|
|
|
|
autoSizeResizeTo(new Vector2(
|
|
AutoSizeAxes.HasFlagFast(Axes.X) ? b.X : base.Width,
|
|
AutoSizeAxes.HasFlagFast(Axes.Y) ? b.Y : base.Height
|
|
), AutoSizeDuration, AutoSizeEasing);
|
|
|
|
//note that this is called before autoSize becomes valid. may be something to consider down the line.
|
|
//might work better to add an OnRefresh event in Cached<> and invoke there.
|
|
OnAutoSize?.Invoke();
|
|
}
|
|
|
|
private void updateChildrenSizeDependencies()
|
|
{
|
|
isComputingChildrenSizeDependencies = true;
|
|
|
|
try
|
|
{
|
|
if (!childrenSizeDependencies.IsValid)
|
|
{
|
|
updateAutoSize();
|
|
childrenSizeDependencies.Validate();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
isComputingChildrenSizeDependencies = false;
|
|
}
|
|
}
|
|
|
|
private void autoSizeResizeTo(Vector2 newSize, double duration = 0, Easing easing = Easing.None)
|
|
{
|
|
var currentTransform = TransformsForTargetMember(nameof(baseSize)).FirstOrDefault() as AutoSizeTransform;
|
|
|
|
if ((currentTransform?.EndValue ?? Size) != newSize)
|
|
{
|
|
if (duration == 0)
|
|
{
|
|
if (currentTransform != null)
|
|
ClearTransforms(false, nameof(baseSize));
|
|
baseSize = newSize;
|
|
}
|
|
else
|
|
this.TransformTo(this.PopulateTransform(new AutoSizeTransform { Rewindable = false }, newSize, duration, easing));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A helper property for <see cref="autoSizeResizeTo(Vector2, double, Easing)"/> to change the size of <see cref="CompositeDrawable"/>s with <see cref="AutoSizeAxes"/>.
|
|
/// </summary>
|
|
private Vector2 baseSize
|
|
{
|
|
get => new Vector2(base.Width, base.Height);
|
|
set
|
|
{
|
|
base.Width = value.X;
|
|
base.Height = value.Y;
|
|
}
|
|
}
|
|
|
|
private class AutoSizeTransform : TransformCustom<Vector2, CompositeDrawable>
|
|
{
|
|
public AutoSizeTransform()
|
|
: base(nameof(baseSize))
|
|
{
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|