using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Theriapolis.Game.CodexUI.Core; public enum HAlign { Left, Center, Right, Stretch } public enum VAlign { Top, Middle, Bottom, Stretch } /// /// Container widget that stacks its children vertically with /// between them. Children are laid out top-to-bottom; their horizontal alignment /// is governed by (defaults to ). /// public sealed class Column : CodexWidget { public System.Collections.Generic.List Children { get; } = new(); public int Spacing { get; set; } = CodexDensity.RowGap; public Thickness Padding { get; set; } public HAlign HAlignChildren { get; set; } = HAlign.Stretch; public Column Add(CodexWidget c) { c.Parent = this; Children.Add(c); return this; } protected override Point MeasureCore(Point available) { int innerW = available.X - Padding.HorizontalSum(); int innerH = available.Y - Padding.VerticalSum(); int totalH = 0, maxW = 0; for (int i = 0; i < Children.Count; i++) { if (!Children[i].Visible) continue; var s = Children[i].Measure(new Point(innerW, innerH)); totalH += s.Y; if (i < Children.Count - 1) totalH += Spacing; if (s.X > maxW) maxW = s.X; } return new Point(maxW + Padding.HorizontalSum(), totalH + Padding.VerticalSum()); } protected override void ArrangeCore(Rectangle bounds) { int x = bounds.X + Padding.Left; int y = bounds.Y + Padding.Top; int innerW = bounds.Width - Padding.HorizontalSum(); foreach (var c in Children) { if (!c.Visible) continue; int cw = HAlignChildren == HAlign.Stretch ? innerW : System.Math.Min(c.DesiredSize.X, innerW); int cx = HAlignChildren switch { HAlign.Center => x + (innerW - cw) / 2, HAlign.Right => x + innerW - cw, _ => x, }; c.Arrange(new Rectangle(cx, y, cw, c.DesiredSize.Y)); y += c.DesiredSize.Y + Spacing; } } public override void Update(GameTime gt, CodexInput input) { foreach (var c in Children) if (c.Visible) c.Update(gt, input); } public override void Draw(SpriteBatch sb, GameTime gt) { foreach (var c in Children) if (c.Visible) c.Draw(sb, gt); } } /// /// Container widget that stacks its children horizontally. Mirrors /// along the perpendicular axis; vertical alignment via . /// public sealed class Row : CodexWidget { public System.Collections.Generic.List Children { get; } = new(); public int Spacing { get; set; } = CodexDensity.ColGap; public Thickness Padding { get; set; } public VAlign VAlignChildren { get; set; } = VAlign.Top; public Row Add(CodexWidget c) { c.Parent = this; Children.Add(c); return this; } protected override Point MeasureCore(Point available) { int innerW = available.X - Padding.HorizontalSum(); int innerH = available.Y - Padding.VerticalSum(); int totalW = 0, maxH = 0; for (int i = 0; i < Children.Count; i++) { if (!Children[i].Visible) continue; var s = Children[i].Measure(new Point(innerW, innerH)); totalW += s.X; if (i < Children.Count - 1) totalW += Spacing; if (s.Y > maxH) maxH = s.Y; } return new Point(totalW + Padding.HorizontalSum(), maxH + Padding.VerticalSum()); } protected override void ArrangeCore(Rectangle bounds) { int x = bounds.X + Padding.Left; int y = bounds.Y + Padding.Top; int innerH = bounds.Height - Padding.VerticalSum(); foreach (var c in Children) { if (!c.Visible) continue; int cy = VAlignChildren switch { VAlign.Middle => y + (innerH - c.DesiredSize.Y) / 2, VAlign.Bottom => y + innerH - c.DesiredSize.Y, VAlign.Stretch => y, _ => y, }; int ch = VAlignChildren == VAlign.Stretch ? innerH : c.DesiredSize.Y; c.Arrange(new Rectangle(x, cy, c.DesiredSize.X, ch)); x += c.DesiredSize.X + Spacing; } } public override void Update(GameTime gt, CodexInput input) { foreach (var c in Children) if (c.Visible) c.Update(gt, input); } public override void Draw(SpriteBatch sb, GameTime gt) { foreach (var c in Children) if (c.Visible) c.Draw(sb, gt); } } /// /// Wrap-flow layout: lays children left-to-right, breaking to a new row when /// the running width would exceed the container. Used for chip rows and the /// review screen's starting-kit grid. /// public sealed class WrapRow : CodexWidget { public System.Collections.Generic.List Children { get; } = new(); public int HSpacing { get; set; } = CodexDensity.ColGap; public int VSpacing { get; set; } = CodexDensity.RowGap; public WrapRow Add(CodexWidget c) { c.Parent = this; Children.Add(c); return this; } protected override Point MeasureCore(Point available) { int innerW = available.X; int x = 0, y = 0, rowMaxH = 0, totalW = 0; foreach (var c in Children) { if (!c.Visible) continue; var s = c.Measure(new Point(innerW, available.Y)); if (x > 0 && x + s.X > innerW) { x = 0; y += rowMaxH + VSpacing; rowMaxH = 0; } x += s.X + HSpacing; if (s.Y > rowMaxH) rowMaxH = s.Y; if (x > totalW) totalW = x; } y += rowMaxH; return new Point(System.Math.Min(totalW, innerW), y); } protected override void ArrangeCore(Rectangle bounds) { int x = bounds.X, y = bounds.Y, rowMaxH = 0; foreach (var c in Children) { if (!c.Visible) continue; if (x > bounds.X && x + c.DesiredSize.X > bounds.X + bounds.Width) { x = bounds.X; y += rowMaxH + VSpacing; rowMaxH = 0; } c.Arrange(new Rectangle(x, y, c.DesiredSize.X, c.DesiredSize.Y)); x += c.DesiredSize.X + HSpacing; if (c.DesiredSize.Y > rowMaxH) rowMaxH = c.DesiredSize.Y; } } public override void Update(GameTime gt, CodexInput input) { foreach (var c in Children) if (c.Visible) c.Update(gt, input); } public override void Draw(SpriteBatch sb, GameTime gt) { foreach (var c in Children) if (c.Visible) c.Draw(sb, gt); } } /// /// Fixed-column grid: lays children left-to-right then wraps. Each cell gets /// (innerWidth - colGap*(columns-1)) / columns of horizontal space. Used for /// the card grid on Clade / Species / Class / Background steps. /// public sealed class Grid : CodexWidget { public System.Collections.Generic.List Children { get; } = new(); public int Columns { get; set; } = 2; public int HSpacing { get; set; } = CodexDensity.CardGap; public int VSpacing { get; set; } = CodexDensity.CardGap; public Grid Add(CodexWidget c) { c.Parent = this; Children.Add(c); return this; } protected override Point MeasureCore(Point available) { int cols = System.Math.Max(1, Columns); int cellW = (available.X - HSpacing * (cols - 1)) / cols; int rowH = 0, totalH = 0, col = 0; foreach (var c in Children) { if (!c.Visible) continue; var s = c.Measure(new Point(cellW, available.Y)); if (s.Y > rowH) rowH = s.Y; col++; if (col >= cols) { totalH += rowH + VSpacing; rowH = 0; col = 0; } } if (col > 0) totalH += rowH; else if (totalH >= VSpacing) totalH -= VSpacing; return new Point(available.X, totalH); } protected override void ArrangeCore(Rectangle bounds) { int cols = System.Math.Max(1, Columns); int cellW = (bounds.Width - HSpacing * (cols - 1)) / cols; int x = bounds.X, y = bounds.Y, col = 0, rowH = 0; foreach (var c in Children) { if (!c.Visible) continue; int ch = c.DesiredSize.Y; c.Arrange(new Rectangle(x, y, cellW, ch)); if (ch > rowH) rowH = ch; col++; if (col >= cols) { y += rowH + VSpacing; x = bounds.X; col = 0; rowH = 0; } else { x += cellW + HSpacing; } } } public override void Update(GameTime gt, CodexInput input) { foreach (var c in Children) if (c.Visible) c.Update(gt, input); } public override void Draw(SpriteBatch sb, GameTime gt) { foreach (var c in Children) if (c.Visible) c.Draw(sb, gt); } } /// /// Single-child decorator that adds breathing room. Combine with any widget /// that doesn't expose its own padding field. /// public sealed class Padding : CodexWidget { public CodexWidget? Child { get; set; } public Thickness Inset { get; set; } public Padding(CodexWidget child, Thickness inset) { Child = child; if (child is not null) child.Parent = this; Inset = inset; } protected override Point MeasureCore(Point available) { if (Child is null) return new Point(Inset.HorizontalSum(), Inset.VerticalSum()); var inner = new Point(available.X - Inset.HorizontalSum(), available.Y - Inset.VerticalSum()); var s = Child.Measure(inner); return new Point(s.X + Inset.HorizontalSum(), s.Y + Inset.VerticalSum()); } protected override void ArrangeCore(Rectangle bounds) { Child?.Arrange(new Rectangle( bounds.X + Inset.Left, bounds.Y + Inset.Top, bounds.Width - Inset.HorizontalSum(), bounds.Height - Inset.VerticalSum())); } public override void Update(GameTime gt, CodexInput input) => Child?.Update(gt, input); public override void Draw(SpriteBatch sb, GameTime gt) => Child?.Draw(sb, gt); } /// Box-model insets: independent left/top/right/bottom in pixels. public readonly struct Thickness { public readonly int Left, Top, Right, Bottom; public Thickness(int uniform) : this(uniform, uniform, uniform, uniform) { } public Thickness(int l, int t, int r, int b) { Left = l; Top = t; Right = r; Bottom = b; } public int HorizontalSum() => Left + Right; public int VerticalSum() => Top + Bottom; }