b451f83174
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
291 lines
11 KiB
C#
291 lines
11 KiB
C#
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 }
|
|
|
|
/// <summary>
|
|
/// Container widget that stacks its children vertically with <see cref="Spacing"/>
|
|
/// between them. Children are laid out top-to-bottom; their horizontal alignment
|
|
/// is governed by <see cref="HAlignChildren"/> (defaults to <see cref="HAlign.Stretch"/>).
|
|
/// </summary>
|
|
public sealed class Column : CodexWidget
|
|
{
|
|
public System.Collections.Generic.List<CodexWidget> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Container widget that stacks its children horizontally. Mirrors <see cref="Column"/>
|
|
/// along the perpendicular axis; vertical alignment via <see cref="VAlignChildren"/>.
|
|
/// </summary>
|
|
public sealed class Row : CodexWidget
|
|
{
|
|
public System.Collections.Generic.List<CodexWidget> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class WrapRow : CodexWidget
|
|
{
|
|
public System.Collections.Generic.List<CodexWidget> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class Grid : CodexWidget
|
|
{
|
|
public System.Collections.Generic.List<CodexWidget> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Single-child decorator that adds breathing room. Combine with any widget
|
|
/// that doesn't expose its own padding field.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Box-model insets: independent left/top/right/bottom in pixels.</summary>
|
|
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;
|
|
}
|