diff --git a/src/Forma.Testing/UiMatrix.cs b/src/Forma.Testing/UiMatrix.cs
new file mode 100644
index 00000000..6dbee8c9
--- /dev/null
+++ b/src/Forma.Testing/UiMatrix.cs
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) Igor Hipólito Vieira
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace Forma
+{
+ /// One cell of a run: the view, locale, UI scale and viewport it was laid out with.
+ public sealed class UiMatrixCase
+ {
+ /// Creates a case.
+ public UiMatrixCase(string view, string locale, int scalePercent, Vector2 viewport, float displayScale, Vector2 logicalViewport)
+ {
+ View = view;
+ Locale = locale;
+ ScalePercent = scalePercent;
+ Viewport = viewport;
+ DisplayScale = displayScale;
+ LogicalViewport = logicalViewport;
+ }
+
+ /// The name of the view.
+ public string View { get; }
+ /// The locale the view was built for.
+ public string Locale { get; }
+ /// The user's UI scale percent (100 is the default).
+ public int ScalePercent { get; }
+ /// The back buffer size in pixels.
+ public Vector2 Viewport { get; }
+ /// The physical-to-logical scale the case lays out with: back buffer height over the design height, times the scale percent.
+ public float DisplayScale { get; }
+ /// The viewport in logical UI units, which is what the controls are laid out in.
+ public Vector2 LogicalViewport { get; }
+ /// View, locale, scale and viewport as one line.
+ public override string ToString() => $"{View} | {Locale} | {ScalePercent}% | {Viewport.X:0}x{Viewport.Y:0}";
+ }
+
+ /// A named view a run lays out; the factory receives the case so it can build the view for that locale.
+ public sealed class UiMatrixView
+ {
+ /// Creates a view entry.
+ public UiMatrixView(string name, Func create)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ Create = create ?? throw new ArgumentNullException(nameof(create));
+ }
+
+ /// The name reported in failures.
+ public string Name { get; }
+ /// Builds the view for a case.
+ public Func Create { get; }
+ }
+
+ /// A failure found in one cell of the matrix.
+ public sealed class UiMatrixFailure
+ {
+ /// Creates a failure.
+ public UiMatrixFailure(UiMatrixCase matrixCase, string message)
+ {
+ Case = matrixCase;
+ Message = message;
+ }
+
+ /// The cell that failed.
+ public UiMatrixCase Case { get; }
+ /// What the check reported.
+ public string Message { get; }
+ /// The case and the message as one line.
+ public override string ToString() => $"{Case}: {Message}";
+ }
+
+ ///
+ /// Lays every view out for every locale, UI scale and viewport and runs a check on each combination, so a layout that only breaks in one
+ /// language at one scale on one window size is found, and named with all four coordinates. The helper owns the host and disposes it.
+ ///
+ public static class UiMatrix
+ {
+ /// The height the UI is authored for; the display scale is the back buffer height over this, times the user's scale.
+ public const float DefaultDesignHeight = 720f;
+
+ /// Lays out each combination and returns the check's messages as failures. The check receives the laid-out context, the view's root and the case.
+ public static IReadOnlyList Run(
+ IEnumerable views,
+ IEnumerable locales,
+ IEnumerable scalePercents,
+ IEnumerable viewports,
+ Func> check,
+ float designHeight = DefaultDesignHeight,
+ Action configureContext = null)
+ {
+ if (views == null) throw new ArgumentNullException(nameof(views));
+ if (locales == null) throw new ArgumentNullException(nameof(locales));
+ if (scalePercents == null) throw new ArgumentNullException(nameof(scalePercents));
+ if (viewports == null) throw new ArgumentNullException(nameof(viewports));
+ if (check == null) throw new ArgumentNullException(nameof(check));
+ var failures = new List();
+ var localeList = locales.ToList();
+ var scaleList = scalePercents.ToList();
+ var viewportList = viewports.ToList();
+ foreach (var view in views)
+ foreach (var locale in localeList)
+ foreach (var scale in scaleList)
+ foreach (var viewport in viewportList)
+ {
+ var displayScale = viewport.Y / designHeight * scale / 100f;
+ var logical = new Vector2(viewport.X / displayScale, viewport.Y / displayScale);
+ var matrixCase = new UiMatrixCase(view.Name, locale, scale, viewport, displayScale, logical);
+ using var context = new UIContext { DisplayScale = displayScale, ViewportSize = logical };
+ configureContext?.Invoke(context);
+ var root = new Control { Size = logical };
+ var control = view.Create(matrixCase);
+ control.Size = logical;
+ root.AddChild(control);
+ context.Add(root);
+ context.Layout();
+ foreach (var message in check(context, root, matrixCase)) failures.Add(new UiMatrixFailure(matrixCase, message));
+ }
+
+ return failures;
+ }
+
+ /// A check that reports every finding.
+ public static Func> LayoutProblems(LayoutProblemOptions options = null) =>
+ (context, root, _) => context.FindLayoutProblems(root, options).Select(problem => $"{problem.Kind}: {problem.Message}");
+ }
+}
diff --git a/tests/Forma.Tests/UiMatrixTest.cs b/tests/Forma.Tests/UiMatrixTest.cs
new file mode 100644
index 00000000..b943d40f
--- /dev/null
+++ b/tests/Forma.Tests/UiMatrixTest.cs
@@ -0,0 +1,81 @@
+// Copyright (c) 2026 Igor Hipólito Vieira
+// SPDX-License-Identifier: MIT
+
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace Forma.Tests
+{
+ [TestFixture]
+ public sealed class UiMatrixTest
+ {
+ private static readonly Vector2[] Viewports = { new Vector2(1280, 720), new Vector2(1920, 1080) };
+
+ // A label a quarter of the logical width wide: it fits the short text everywhere and the long text only while the logical
+ // viewport is wide, so it breaks in exactly one language at one scale.
+ private static IReadOnlyList Run(UIFontFace face, out int cases)
+ {
+ var count = 0;
+ var views = new[]
+ {
+ new UiMatrixView("tight label", matrixCase =>
+ {
+ var text = matrixCase.Locale == "long" ? "Auto-place Spawn Points now" : "Save";
+ var label = new Label { Text = text, UIFont = new DynamicUIFont(face, 16), Padding = new Thickness(0), Position = new Vector2(4, 4), Size = new Vector2(matrixCase.LogicalViewport.X / 4, 30) };
+ var host = new Control();
+ host.AddChild(label);
+ return host;
+ }),
+ };
+
+ var failures = UiMatrix.Run(views, new[] { "short", "long" }, new[] { 100, 200 }, Viewports,
+ (context, root, matrixCase) =>
+ {
+ count++;
+ return UiMatrix.LayoutProblems().Invoke(context, root, matrixCase);
+ });
+ cases = count;
+ return failures;
+ }
+
+ [Test]
+ public void ABrokenView_FailsInExactlyTheExpectedCombinations_AndNoOther()
+ {
+ using var face = UIFontFace.FromProjectFile(TestContext.CurrentContext.TestDirectory, "Fonts/Inter_Regular.ttf");
+
+ var failures = Run(face, out var cases);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cases, Is.EqualTo(1 * 2 * 2 * 2));
+ Assert.That(failures.Select(f => f.Case.Locale).Distinct(), Is.EqualTo(new[] { "long" }));
+ Assert.That(failures.Select(f => f.Case.ScalePercent).Distinct(), Is.EqualTo(new[] { 200 }));
+ Assert.That(failures.Select(f => f.Case.Viewport).OrderBy(v => v.X), Is.EqualTo(Viewports));
+ Assert.That(failures[0].ToString(), Does.Contain("tight label").And.Contain("long").And.Contain("200%").And.Contain("TextClipped"));
+ });
+ }
+
+ [Test]
+ public void TheCase_CarriesTheDisplayScaleAndTheLogicalViewport()
+ {
+ UiMatrixCase seen = null;
+
+ UiMatrix.Run(new[] { new UiMatrixView("v", c => new Control()) }, new[] { "en" }, new[] { 130 }, new[] { new Vector2(1920, 1080) },
+ (context, root, matrixCase) => { seen = matrixCase; return System.Array.Empty(); });
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(seen.DisplayScale, Is.EqualTo(1.5f * 1.3f).Within(0.001f));
+ Assert.That(seen.LogicalViewport.X, Is.EqualTo(1920 / seen.DisplayScale).Within(0.01f));
+ });
+ }
+
+ [Test]
+ public void AViewThatFitsEverywhere_ProducesNoFailures()
+ {
+ var failures = UiMatrix.Run(new[] { new UiMatrixView("empty", c => new Control()) }, new[] { "en", "ja" }, new[] { 70, 100, 130 }, Viewports, UiMatrix.LayoutProblems());
+
+ Assert.That(failures, Is.Empty);
+ }
+ }
+}