diff --git a/Dashboard/Controls/ContainerControl.cs b/Dashboard/Controls/ContainerControl.cs index 32fbc59..2edcbae 100644 --- a/Dashboard/Controls/ContainerControl.cs +++ b/Dashboard/Controls/ContainerControl.cs @@ -12,13 +12,22 @@ namespace Dashboard.Controls public bool IsReadOnly => false; + public event EventHandler? ChildAdded; + public event EventHandler? ChildRemoved; + public void Add(Control item) { children.Add(item); + OnChildAdded(new ChildEventArgs(item)); } public void Clear() { + foreach (Control child in this) + { + OnChildRemoved(new ChildEventArgs(child)); + } + children.Clear(); } @@ -39,7 +48,25 @@ namespace Dashboard.Controls public bool Remove(Control item) { - return children.Remove(item); + if (children.Remove(item)) + { + OnChildRemoved(new ChildEventArgs(item)); + return true; + } + + return false; + } + + public virtual void OnChildAdded(ChildEventArgs ea) + { + Adopt(ea.Child, this); + ChildAdded?.Invoke(this, ea); + } + + public virtual void OnChildRemoved(ChildEventArgs ea) + { + Adopt(ea.Child, null); + ChildRemoved?.Invoke(this, ea); } IEnumerator IEnumerable.GetEnumerator() @@ -47,4 +74,13 @@ namespace Dashboard.Controls return children.GetEnumerator(); } } + public class ChildEventArgs : EventArgs + { + public Control Child { get; } + + public ChildEventArgs(Control child) + { + Child = child; + } + } } \ No newline at end of file diff --git a/Dashboard/Controls/Control.cs b/Dashboard/Controls/Control.cs index ba8df72..2c77562 100644 --- a/Dashboard/Controls/Control.cs +++ b/Dashboard/Controls/Control.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Dashboard.ImmediateDraw; +using OpenTK.Mathematics; namespace Dashboard.Controls { @@ -8,6 +9,18 @@ namespace Dashboard.Controls { private readonly DrawList drawCommands = new DrawList(); + public string? Id { get; set; } + + public override Vector2 Position + { + get => base.Position; + set + { + base.Position = value; + InvalidateLayout(); + } + } + public Style Style { get; set; } = new Style(); public float Padding { @@ -20,6 +33,8 @@ namespace Dashboard.Controls protected bool IsLayoutSuspended { get; private set; } = false; + protected ResizedEventArgs? LastResizeEvent { get; private set; } + public void InvalidateVisual() { IsVisualsValid = false; @@ -72,6 +87,14 @@ namespace Dashboard.Controls cmd.Splice(drawCommands); + if (this is IEnumerable children) + { + foreach (Control child in children) + { + child.Paint(cmd); + } + } + cmd.PopViewport(); cmd.PopStyle(); } @@ -108,18 +131,12 @@ namespace Dashboard.Controls LayoutValidated?.Invoke(sender, ea); } - protected void ValidateChildrenLayout() + public override void OnResized(object sender, ResizedEventArgs ea) { - if (this is IEnumerable enumerable) - { - foreach (Control child in enumerable) - { - if (child.IsLayoutValid) - continue; - - child.ValidateLayout(); - } - } + base.OnResized(sender, ea); + + LastResizeEvent = ea; + InvalidateLayout(); } } } \ No newline at end of file diff --git a/Dashboard/Controls/FlowBox.cs b/Dashboard/Controls/FlowBox.cs index 66dffb3..e47394a 100644 --- a/Dashboard/Controls/FlowBox.cs +++ b/Dashboard/Controls/FlowBox.cs @@ -17,7 +17,6 @@ namespace Dashboard.Controls protected override void ValidateLayout() { - ValidateChildrenLayout(); } protected override void ValidateVisual(DrawList cmd) diff --git a/Dashboard/Controls/GridBox.cs b/Dashboard/Controls/GridBox.cs new file mode 100644 index 0000000..9eaf585 --- /dev/null +++ b/Dashboard/Controls/GridBox.cs @@ -0,0 +1,31 @@ + +using Dashboard.ImmediateDraw; +using Dashboard.Layout; +using OpenTK.Mathematics; + +namespace Dashboard.Controls +{ + public class GridBox : ContainerControl + { + protected override void ValidateLayout() + { + if (LastResizeEvent != null) + { + foreach (Control child in this) + { + GridLayoutAttribute attribute = GridLayoutAttribute.GetGridLayout(child); + + attribute.Evaluate(LastResizeEvent, child.Bounds, out Rectangle newBounds); + child.Bounds = newBounds; + + child.InvalidateLayout(); + } + } + } + + protected override void ValidateVisual(DrawList cmd) + { + cmd.Rectangle(new Rectangle(Size, Vector2.Zero)); + } + } +} \ No newline at end of file diff --git a/Dashboard/Controls/UIBase.cs b/Dashboard/Controls/UIBase.cs index a083cf7..3bea5e2 100644 --- a/Dashboard/Controls/UIBase.cs +++ b/Dashboard/Controls/UIBase.cs @@ -6,6 +6,7 @@ using OpenTK.Mathematics; namespace Dashboard.Controls { + /// /// Bases for all UI elements. /// @@ -14,7 +15,6 @@ namespace Dashboard.Controls private Vector2 size; public UIBase? Parent { get; protected set; } - public string? Id { get; set; } public Rectangle Bounds { get => new Rectangle(Position + Size, Position); @@ -25,7 +25,7 @@ namespace Dashboard.Controls } } - public Vector2 Position { get; set; } + public virtual Vector2 Position { get; set; } public Vector2 Size { @@ -85,12 +85,18 @@ namespace Dashboard.Controls } public event EventHandler? Resized; + public event EventHandler? Adopted; public virtual void OnResized(object sender, ResizedEventArgs ea) { Resized?.Invoke(sender, ea); } + protected virtual void OnAdopted(UIBase? parent) + { + Adopted?.Invoke(this, new AdoptedEventArgs(parent)); + } + public bool IsDisposed { get; private set; } = false; protected virtual void Dispose(bool disposing) @@ -113,6 +119,12 @@ namespace Dashboard.Controls } public void Dispose() => DisposeInvoker(true); + + protected static void Adopt(UIBase child, UIBase? parent) + { + child.Parent = parent; + child.OnAdopted(parent); + } } public class ResizedEventArgs : EventArgs @@ -126,4 +138,14 @@ namespace Dashboard.Controls OldSize = oldSize; } } + + public class AdoptedEventArgs : EventArgs + { + public UIBase? Parent { get; } + + public AdoptedEventArgs(UIBase? parent = null) + { + Parent = parent; + } + } } \ No newline at end of file diff --git a/Dashboard/Layout/GridLayoutAttribute.cs b/Dashboard/Layout/GridLayoutAttribute.cs new file mode 100644 index 0000000..2d68295 --- /dev/null +++ b/Dashboard/Layout/GridLayoutAttribute.cs @@ -0,0 +1,116 @@ +using OpenTK.Mathematics; +using Dashboard.Controls; +using System.Reflection.Metadata; + +namespace Dashboard.Layout +{ + /// + /// Control attributes for grid layout. + /// + public class GridLayoutAttribute + { + /// + /// An anchor will keep the relative distance of a control's edges the same during resizes. + /// + /// has higher precedence. + public Anchor Anchor { get; set; } = Anchor.Left | Anchor.Top; + + /// + /// Dock will strongly attach a control to its container or its edges. + /// + /// Has more precedence than + public Dock Dock { get; set; } = Dock.None; + + public void Evaluate( + ResizedEventArgs parentResizeEvent, + in Rectangle oldBounds, + out Rectangle newBounds + ) + { + switch (Dock) + { + default: + case Dock.None: + break; + case Dock.Top: + newBounds = new Rectangle( + parentResizeEvent.NewSize.X, + oldBounds.Size.Y, + 0, + 0 + ); + return; + case Dock.Bottom: + newBounds = new Rectangle( + parentResizeEvent.NewSize.X, + parentResizeEvent.NewSize.Y, + 0, + oldBounds.Size.Y + ); + return; + case Dock.Left: + newBounds = new Rectangle( + oldBounds.Size.X, + parentResizeEvent.NewSize.Y, + 0, + 0 + ); + return; + case Dock.Right: + newBounds = new Rectangle( + parentResizeEvent.NewSize.X, + parentResizeEvent.NewSize.Y, + parentResizeEvent.NewSize.Y - oldBounds.Size.Y, + 0 + ); + return; + } + + Vector2 scale = parentResizeEvent.NewSize / parentResizeEvent.OldSize; + + newBounds = oldBounds; + + if (Anchor.HasFlag(Anchor.Top)) + { + newBounds.Top = scale.Y * oldBounds.Top; + } + + if (Anchor.HasFlag(Anchor.Left)) + { + newBounds.Left = scale.X * oldBounds.Left; + } + + if (Anchor.HasFlag(Anchor.Bottom)) + { + float margin = scale.Y * (parentResizeEvent.OldSize.Y - newBounds.Bottom); + newBounds.Bottom = parentResizeEvent.NewSize.Y - margin; + } + + if (Anchor.HasFlag(Anchor.Right)) + { + float margin = scale.X * (parentResizeEvent.OldSize.X - newBounds.Right); + newBounds.Right = parentResizeEvent.NewSize.X - margin; + } + } + + /// + /// Get the grid layout attribute associated with the given UI-base. + /// + /// The UI-base to query. + /// It's grid layout attribute, if any. + public static GridLayoutAttribute GetGridLayout(UIBase uiBase) + { + const string GRID_LAYOUT_ATTRIBUTE_KEY = "486ddf8c-b75f-4ad4-a51d-5ba20db9bd0e"; + + if ( + !uiBase.Attributes.TryGetValue(GRID_LAYOUT_ATTRIBUTE_KEY, out object? attribute) + || attribute is not GridLayoutAttribute) + { + attribute = new GridLayoutAttribute(); + uiBase.Attributes[GRID_LAYOUT_ATTRIBUTE_KEY] = attribute; + } + + return (GridLayoutAttribute)attribute; + } + } +} \ No newline at end of file