207 lines
5.4 KiB
C#
207 lines
5.4 KiB
C#
using System.Numerics;
|
|
using Voile.Rendering;
|
|
|
|
namespace Voile.UI.Containers;
|
|
|
|
// TODO: make Container extend Widget, it already implements similar behaviors.
|
|
/// <summary>
|
|
/// A base class for all UI containers, used to position and rendering child <see cref="IElement">s.
|
|
/// </summary>
|
|
public abstract class Container : IElement, IParentableElement, IUpdatableElement, IResizeableElement, IRenderableElement, IAnchorableElement
|
|
{
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<IElement> Children => _children;
|
|
/// <inheritdoc />
|
|
public Vector2 Position { get; set; }
|
|
/// <inheritdoc />
|
|
public Rect MinimumRect { get; set; } = Rect.Zero;
|
|
/// <inheritdoc />
|
|
public Rect Size
|
|
{
|
|
get => _size;
|
|
set
|
|
{
|
|
_size = value;
|
|
|
|
if (value.Width < MinimumRect.Width)
|
|
{
|
|
_size.Width = MinimumRect.Width;
|
|
}
|
|
|
|
if (value.Height < MinimumRect.Height)
|
|
{
|
|
_size.Height = MinimumRect.Height;
|
|
}
|
|
|
|
MarkDirty();
|
|
}
|
|
}
|
|
/// <inheritdoc />
|
|
public bool Visible { get; set; } = true;
|
|
/// <inheritdoc />
|
|
public bool Dirty => _isDirty;
|
|
|
|
/// <summary>
|
|
/// Specifies if this <see cref="Container"/>'s minimum size will be confined to contents.
|
|
/// </summary>
|
|
public bool ConfineToContents { get; set; } = false;
|
|
public Anchor Anchor { get; set; }
|
|
public Vector2 AnchorOffset { get; set; }
|
|
|
|
public Container()
|
|
{
|
|
MarkDirty();
|
|
}
|
|
|
|
public Container(Rect minimumSize)
|
|
{
|
|
MinimumRect = minimumSize;
|
|
|
|
MarkDirty();
|
|
}
|
|
|
|
public Container(Rect minimumSize, List<IElement> children)
|
|
{
|
|
MinimumRect = minimumSize;
|
|
_children = children;
|
|
|
|
MarkDirty();
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
if (!_isDirty) return;
|
|
_isDirty = false;
|
|
|
|
|
|
foreach (var child in _children)
|
|
{
|
|
if (child is not IUpdatableElement updatable) continue;
|
|
updatable.Update();
|
|
|
|
if (child is IAnchorableElement anchorable)
|
|
{
|
|
anchorable.ApplyAnchor(Position, Size);
|
|
}
|
|
}
|
|
|
|
Arrange();
|
|
|
|
if (ConfineToContents)
|
|
{
|
|
RecalculateSizes();
|
|
}
|
|
}
|
|
|
|
public void MarkDirty() => _isDirty = true;
|
|
|
|
/// <summary>
|
|
/// Called when this <see cref="Container"/> has to rearrange its children.
|
|
/// </summary>
|
|
public abstract void Arrange();
|
|
|
|
/// <summary>
|
|
/// Recalculates sizes for this <see cref="Container"/>.
|
|
/// This is done automatically if <see cref="ConfineToContents"/> is true.
|
|
/// </summary>
|
|
public void RecalculateSizes()
|
|
{
|
|
float minX = float.MaxValue;
|
|
float minY = float.MaxValue;
|
|
float maxX = float.MinValue;
|
|
float maxY = float.MinValue;
|
|
|
|
foreach (var child in Children)
|
|
{
|
|
var pos = child.Position;
|
|
var size = child.Size;
|
|
|
|
minX = MathF.Min(minX, pos.X);
|
|
minY = MathF.Min(minY, pos.Y);
|
|
maxX = MathF.Max(maxX, pos.X + size.Width);
|
|
maxY = MathF.Max(maxY, pos.Y + size.Height);
|
|
}
|
|
|
|
float padding = 0f;
|
|
|
|
float occupiedWidth = (maxX - minX) + padding * 2;
|
|
float occupiedHeight = (maxY - minY) + padding * 2;
|
|
|
|
float finalWidth = MathF.Max(occupiedWidth, MinimumRect.Width);
|
|
float finalHeight = MathF.Max(occupiedHeight, MinimumRect.Height);
|
|
|
|
MinimumRect = new Rect(finalWidth, finalHeight);
|
|
|
|
if (MinimumRect > _size)
|
|
{
|
|
_size = MinimumRect;
|
|
}
|
|
}
|
|
|
|
public void AddChild(IElement child)
|
|
{
|
|
_children.Add(child);
|
|
|
|
MarkDirty();
|
|
Update();
|
|
}
|
|
|
|
public void RemoveChild(IElement child)
|
|
{
|
|
_children.Remove(child);
|
|
|
|
MarkDirty();
|
|
Update();
|
|
}
|
|
|
|
public void Render(RenderSystem renderer, Style style)
|
|
{
|
|
foreach (var child in Children)
|
|
{
|
|
if (child is not IRenderableElement renderable) continue;
|
|
renderable.Render(renderer, style);
|
|
}
|
|
}
|
|
|
|
public void DrawSize(RenderSystem renderer)
|
|
{
|
|
renderer.SetTransform(Position, Vector2.Zero);
|
|
renderer.DrawRectangleOutline(new Vector2(Size.Width, Size.Height), Color.Red, 2.0f);
|
|
}
|
|
|
|
public void ApplyAnchor(Vector2 parentPosition, Rect parentRect)
|
|
{
|
|
var absoluteOffset = AnchorOffset * new Vector2(Size.Width, Size.Height);
|
|
|
|
switch (Anchor)
|
|
{
|
|
case Anchor.TopLeft:
|
|
Position = parentPosition - AnchorOffset;
|
|
break;
|
|
case Anchor.TopCenter:
|
|
var topCenterX = parentRect.Width / 2;
|
|
var topCenterY = parentPosition.Y;
|
|
|
|
Position = new Vector2(parentPosition.X + topCenterX, topCenterY) - absoluteOffset;
|
|
break;
|
|
case Anchor.TopRight:
|
|
Position = new Vector2(parentPosition.X + parentRect.Width, parentPosition.Y);
|
|
break;
|
|
|
|
case Anchor.Fill:
|
|
Position = parentPosition;
|
|
Size = parentRect;
|
|
break;
|
|
|
|
default:
|
|
throw new NotImplementedException("This anchor type is not implemented!");
|
|
}
|
|
|
|
MarkDirty();
|
|
Update();
|
|
}
|
|
|
|
private List<IElement> _children = new();
|
|
private bool _isDirty;
|
|
private Rect _size = Rect.Zero;
|
|
} |