Files
osu-framework/osu.Framework/Graphics/Containers/Container.cs
2024-02-18 19:43:17 +01:00

548 lines
20 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 System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Effects;
using osu.Framework.Lists;
using osuTK;
namespace osu.Framework.Graphics.Containers
{
/// <summary>
/// A drawable which can have children added to it. Transformations applied to
/// a container are also applied to its children.
/// Additionally, containers support various effects, such as masking, edge effect,
/// padding, and automatic sizing depending on their children.
/// If all children are of a specific non-<see cref="Drawable"/> type, use the
/// generic version <see cref="Container{T}"/>.
/// </summary>
public partial class Container : Container<Drawable>
{
}
/// <summary>
/// A drawable which can have children added to it. Transformations applied to
/// a container are also applied to its children.
/// Additionally, containers support various effects, such as masking, edge effect,
/// padding, and automatic sizing depending on their children.
/// </summary>
public partial class Container<T> : CompositeDrawable, IContainerEnumerable<T>, IContainerCollection<T>, ICollection<T>, IReadOnlyList<T>
where T : Drawable
{
/// <summary>
/// This is checked when enumerating through this <see cref="Container"/> to throw when
/// <see cref="Children"/> was mutated while enumerating (in <see cref="Enumerator"/>).
/// This is incremented whenever <see cref="Children"/> is mutated (e.g. with <see cref="Add(T)"/>).
/// </summary>
private int enumeratorVersion;
/// <summary>
/// Constructs a <see cref="Container"/> that stores children.
/// </summary>
public Container()
{
if (typeof(T) == typeof(Drawable))
internalChildrenAsT = (IReadOnlyList<T>)InternalChildren;
else
internalChildrenAsT = new LazyList<Drawable, T>(InternalChildren, c => (T)c);
if (typeof(T) == typeof(Drawable))
aliveInternalChildrenAsT = (IReadOnlyList<T>)AliveInternalChildren;
else
aliveInternalChildrenAsT = new LazyList<Drawable, T>(AliveInternalChildren, c => (T)c);
}
/// <summary>
/// The content of this container. <see cref="Children"/> and all methods that mutate
/// <see cref="Children"/> (e.g. <see cref="Add(T)"/> and <see cref="Remove(T, bool)"/>) are
/// forwarded to the content. By default a container's content is itself, in which case
/// <see cref="Children"/> refers to <see cref="CompositeDrawable.InternalChildren"/>.
/// This property is useful for containers that require internal children that should
/// not be exposed to the outside world, e.g. <see cref="ScrollContainer{T}"/>.
/// </summary>
protected virtual Container<T> Content => this;
/// <summary>
/// The publicly accessible list of children. Forwards to the children of <see cref="Content"/>.
/// If <see cref="Content"/> is this container, then returns <see cref="CompositeDrawable.InternalChildren"/>.
/// Assigning to this property will dispose all existing children of this Container.
/// <remarks>
/// If a foreach loop is used, iterate over the <see cref="Container"/> directly rather than its <see cref="Children"/>.
/// </remarks>
/// </summary>
public IReadOnlyList<T> Children
{
get
{
if (Content != this)
return Content.Children;
return internalChildrenAsT;
}
set => ChildrenEnumerable = value;
}
/// <summary>
/// The publicly accessible list of alive children. Forwards to the alive children of <see cref="Content"/>.
/// If <see cref="Content"/> is this container, then returns <see cref="CompositeDrawable.AliveInternalChildren"/>.
/// </summary>
public IReadOnlyList<T> AliveChildren
{
get
{
if (Content != this)
return Content.AliveChildren;
return aliveInternalChildrenAsT;
}
}
/// <summary>
/// Accesses the <paramref name="index"/>-th child.
/// </summary>
/// <param name="index">The index of the child to access.</param>
/// <returns>The <paramref name="index"/>-th child.</returns>
public T this[int index] => Children[index];
/// <summary>
/// The amount of elements in <see cref="Children"/>.
/// </summary>
public int Count => Children.Count;
/// <summary>
/// Whether this <see cref="Container{T}"/> can have elements added and removed. Always false.
/// </summary>
public bool IsReadOnly => false;
/// <summary>
/// Copies the elements of the <see cref="Container{T}"/> to an Array, starting at a particular Array index.
/// </summary>
/// <param name="array">The Array into which all children should be copied.</param>
/// <param name="arrayIndex">The starting index in the Array.</param>
public void CopyTo(T[] array, int arrayIndex)
{
foreach (var c in Children)
array[arrayIndex++] = c;
}
bool ICollection<T>.Remove(T item)
{
ArgumentNullException.ThrowIfNull(item);
return Remove(item, true);
}
public Enumerator GetEnumerator() => new Enumerator(this);
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Sets all children of this container to the elements contained in the enumerable.
/// </summary>
public IEnumerable<T> ChildrenEnumerable
{
set
{
if (IsDisposed)
return;
Clear();
AddRange(value);
}
}
/// <summary>
/// Gets or sets the only child of this container.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public T Child
{
get
{
if (Children.Count != 1)
throw new InvalidOperationException($"Cannot call {nameof(InternalChild)} unless there's exactly one {nameof(Drawable)} in {nameof(Children)} (currently {Children.Count})!");
return Children[0];
}
set
{
if (IsDisposed)
return;
Clear();
Add(value);
}
}
private readonly IReadOnlyList<T> internalChildrenAsT;
private readonly IReadOnlyList<T> aliveInternalChildrenAsT;
/// <summary>
/// The index of a given child within <see cref="Children"/>.
/// </summary>
/// <returns>
/// If the child is found, its index. Otherwise, the negated index it would obtain
/// if it were added to <see cref="Children"/>.
/// </returns>
public int IndexOf(T drawable)
{
if (Content != this)
return Content.IndexOf(drawable);
return IndexOfInternal(drawable);
}
/// <summary>
/// Checks whether a given child is contained within <see cref="Children"/>.
/// </summary>
public bool Contains(T drawable)
{
int index = IndexOf(drawable);
return index >= 0 && this[index] == drawable;
}
/// <summary>
/// Adds a child to this container. This amounts to adding a child to <see cref="Content"/>'s
/// <see cref="Children"/>, recursing until <see cref="Content"/> == this.
/// </summary>
public virtual void Add(T drawable)
{
if (drawable == Content)
throw new InvalidOperationException("Content may not be added to itself.");
ArgumentNullException.ThrowIfNull(drawable);
ObjectDisposedException.ThrowIf(drawable.IsDisposed, drawable);
if (Content == this)
AddInternal(drawable);
else
Content.Add(drawable);
}
/// <summary>
/// Adds a range of children. This is equivalent to calling <see cref="Add(T)"/> on
/// each element of the range in order.
/// </summary>
public void AddRange(IEnumerable<T> 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 (T d in range)
Add(d);
}
protected override void AddInternal(Drawable drawable)
{
if (Content == this && drawable != null && !(drawable is T))
{
throw new InvalidOperationException(
$"Only {typeof(T).ReadableName()} type drawables may be added to a container of type {GetType().ReadableName()} which does not redirect {nameof(Content)}.");
}
enumeratorVersion++;
base.AddInternal(drawable);
}
/// <summary>
/// Removes a given child from this container.
/// </summary>
public virtual bool Remove(T drawable, bool disposeImmediately)
{
if (Content != this)
return Content.Remove(drawable, disposeImmediately);
return RemoveInternal(drawable, disposeImmediately);
}
/// <summary>
/// Removes all children which match the given predicate.
/// This is equivalent to calling <see cref="Remove(T, bool)"/> for each child that
/// matches the given predicate.
/// </summary>
/// <returns>The amount of removed children.</returns>
public int RemoveAll(Predicate<T> pred, bool disposeImmediately)
{
if (Content != this)
return Content.RemoveAll(pred, disposeImmediately);
int removedCount = 0;
for (int i = 0; i < InternalChildren.Count; i++)
{
var tChild = (T)InternalChildren[i];
if (pred.Invoke(tChild))
{
RemoveInternal(tChild, disposeImmediately);
removedCount++;
i--;
}
}
return removedCount;
}
/// <summary>
/// Removes a range of children. This is equivalent to calling <see cref="Remove(T, bool)"/> on
/// each element of the range in order.
/// </summary>
public void RemoveRange([CanBeNull] IEnumerable<T> range, bool disposeImmediately)
{
if (range == null)
return;
foreach (T p in range)
Remove(p, disposeImmediately);
}
protected internal override bool RemoveInternal(Drawable drawable, bool disposeImmediately)
{
enumeratorVersion++;
return base.RemoveInternal(drawable, disposeImmediately);
}
/// <summary>
/// Removes all children.
/// </summary>
public void Clear() => Clear(true);
/// <summary>
/// Removes all children.
/// </summary>
/// <param name="disposeChildren">
/// Whether removed children should also get disposed.
/// Disposal will be recursive.
/// </param>
public virtual void Clear(bool disposeChildren)
{
if (Content != null && Content != this)
Content.Clear(disposeChildren);
else
ClearInternal(disposeChildren);
}
protected internal override void ClearInternal(bool disposeChildren = true)
{
enumeratorVersion++;
base.ClearInternal(disposeChildren);
}
/// <summary>
/// Changes the depth of a child. This affects ordering of children within this container.
/// </summary>
/// <param name="child">The child whose depth is to be changed.</param>
/// <param name="newDepth">The new depth value to be set.</param>
public void ChangeChildDepth(T child, float newDepth)
{
if (Content != this)
Content.ChangeChildDepth(child, newDepth);
else
ChangeInternalChildDepth(child, newDepth);
}
/// <summary>
/// If enabled, only the portion of children that falls within this <see cref="Container"/>'s
/// shape is drawn to the screen.
/// </summary>
public new bool Masking
{
get => base.Masking;
set => base.Masking = value;
}
/// <summary>
/// Determines over how many pixels the alpha component smoothly fades out when an inner <see cref="EdgeEffect"/> or <see cref="BorderThickness"/> is present.
/// Only has an effect when <see cref="Masking"/> is true.
/// </summary>
public new float MaskingSmoothness
{
get => base.MaskingSmoothness;
set => base.MaskingSmoothness = value;
}
/// <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 new float CornerRadius
{
get => base.CornerRadius;
set => base.CornerRadius = value;
}
/// <summary>
/// Determines how gentle the curve of the corner straightens. A value of 2 results in
/// circular arcs, a value of 2.5 (default) 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 new float CornerExponent
{
get => base.CornerExponent;
set => base.CornerExponent = value;
}
/// <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 new float BorderThickness
{
get => base.BorderThickness;
set => base.BorderThickness = value;
}
/// <summary>
/// Determines the color of the border controlled by <see cref="BorderThickness"/>.
/// Only has an effect when <see cref="Masking"/> is true.
/// </summary>
public new ColourInfo BorderColour
{
get => base.BorderColour;
set => base.BorderColour = value;
}
/// <summary>
/// Determines an edge effect of this <see cref="Container"/>.
/// Edge effects are e.g. glow or a shadow.
/// Only has an effect when <see cref="Masking"/> is true.
/// </summary>
public new EdgeEffectParameters EdgeEffect
{
get => base.EdgeEffect;
set => base.EdgeEffect = value;
}
/// <summary>
/// Shrinks the space children may occupy within this <see cref="Container"/>
/// by the specified amount on each side.
/// </summary>
public new MarginPadding Padding
{
get => base.Padding;
set => base.Padding = value;
}
/// <summary>
/// Whether to use a local vertex batch for rendering. If false, a parenting vertex batch will be used.
/// </summary>
public new bool ForceLocalVertexBatch
{
get => base.ForceLocalVertexBatch;
set => base.ForceLocalVertexBatch = value;
}
/// <summary>
/// The size of the relative position/size coordinate space of children of this <see cref="Container"/>.
/// 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="Container"/>.
/// </summary>
public new Vector2 RelativeChildSize
{
get => base.RelativeChildSize;
set => base.RelativeChildSize = value;
}
/// <summary>
/// The offset of the relative position/size coordinate space of children of this <see cref="Container"/>.
/// 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="Container"/>.
/// </summary>
public new Vector2 RelativeChildOffset
{
get => base.RelativeChildOffset;
set => base.RelativeChildOffset = value;
}
/// <summary>
/// Controls which <see cref="Axes"/> are automatically sized w.r.t. <see cref="CompositeDrawable.InternalChildren"/>.
/// Children's <see cref="Drawable.BypassAutoSizeAxes"/> are ignored for automatic sizing.
/// Most notably, <see cref="Drawable.RelativePositionAxes"/> and <see cref="Drawable.RelativeSizeAxes"/> of children
/// do not affect automatic sizing to avoid circular size dependencies.
/// It is not allowed to manually set <see cref="Drawable.Size"/> (or <see cref="Drawable.Width"/> / <see cref="Drawable.Height"/>)
/// on any <see cref="Axes"/> which are automatically sized.
/// </summary>
public new Axes AutoSizeAxes
{
get => base.AutoSizeAxes;
set => base.AutoSizeAxes = value;
}
/// <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 new float AutoSizeDuration
{
get => base.AutoSizeDuration;
set => base.AutoSizeDuration = value;
}
/// <summary>
/// The type of easing which should be used for smooth automatic sizing when <see cref="AutoSizeDuration"/>
/// is non-zero.
/// </summary>
public new Easing AutoSizeEasing
{
get => base.AutoSizeEasing;
set => base.AutoSizeEasing = value;
}
public struct Enumerator : IEnumerator<T>
{
private Container<T> container;
private int currentIndex;
private readonly int version;
internal Enumerator(Container<T> container)
{
this.container = container;
currentIndex = -1; // The first MoveNext() should bring the iterator to 0
version = container.enumeratorVersion;
}
public bool MoveNext()
{
if (version != container.enumeratorVersion)
throw new InvalidOperationException($"May not add or remove {nameof(Children)} from this {nameof(Container)} during enumeration.");
return ++currentIndex < container.Count;
}
public void Reset() => currentIndex = -1;
public readonly T Current => container[currentIndex];
readonly object IEnumerator.Current => Current;
public void Dispose()
{
container = null;
}
}
}
}