- TargetFrameworks net48;net40. net48 keeps x64 + ViGEm + System.IO.Ports package; net40 adds Microsoft.Bcl.Async + System.ValueTuple and uses the in-box SerialPort. - Compat/TaskCompat bridges Task.Run/Delay/WhenAny/WhenAll (TaskEx on net40) and SemaphoreSlim.WaitAsync (net40 blocks briefly - trivial at 9600 baud). - IReadOnlyList/IReadOnlyDictionary -> IList/IDictionary throughout (net40 predates the IReadOnly* interfaces and the Bcl backport cannot make arrays implement them). - HashCode.Combine replaced with a manual combine (Bcl.HashCode has no net40 build); Marshal.SizeOf<T> -> typeof form; ViGEmJoystickSink gated #if !NET40. HidFeederJoystickSink stays on both flavors - it will drive RioGamepadXP.sys on XP via the same contract. Both TFMs build; 275 tests green on net48. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
144 lines
5.1 KiB
C#
144 lines
5.1 KiB
C#
using RioJoy.Core.Overlay;
|
|
using Xunit;
|
|
|
|
namespace RioJoy.Core.Tests.Overlay;
|
|
|
|
public class OverlayLayoutEngineTests
|
|
{
|
|
// LinearTextMeasurer(0.6, 1.2): width = len*size*0.6, height = size*1.2.
|
|
private static readonly OverlayLayoutEngine Engine = new(new LinearTextMeasurer());
|
|
|
|
private static OverlayRegion Region(
|
|
string name = "b-00", double x = 0, double y = 0, double w = 200, double h = 40, double size = 10) =>
|
|
new() { Name = name, X = x, Y = y, Width = w, Height = h, FontSizePx = size, FontFamily = "Sans" };
|
|
|
|
[Fact]
|
|
public void Place_TextFits_KeepsBaseSize_AndCenters()
|
|
{
|
|
// "FIRE" at size 10 -> 24 x 12, well inside 200 x 40.
|
|
PlacedLabel p = Engine.Place(Region(x: 10, y: 20), "FIRE");
|
|
|
|
Assert.Equal(10, p.FontSizePx, 6);
|
|
Assert.Equal(24, p.Width, 6);
|
|
Assert.Equal(12, p.Height, 6);
|
|
Assert.Equal(10 + (200 - 24) / 2, p.X, 6); // 98
|
|
Assert.Equal(20 + (40 - 12) / 2, p.Y, 6); // 34
|
|
}
|
|
|
|
[Fact]
|
|
public void Place_Overflow_Fit_ShrinksFont()
|
|
{
|
|
// Narrow cell: "FIRE" at base 10 is 24 wide > 20 -> auto-fit shrinks it.
|
|
PlacedLabel p = Engine.Place(Region(w: 20, h: 40), "FIRE");
|
|
|
|
Assert.True(p.FontSizePx < 10);
|
|
Assert.True(p.Width <= 20, $"fitted width {p.Width} must be <= 20");
|
|
}
|
|
|
|
[Fact]
|
|
public void Place_Overflow_Crop_KeepsBaseSize()
|
|
{
|
|
var opts = new OverlayLayoutOptions { SizeMode = OverlaySizeMode.Crop };
|
|
PlacedLabel p = Engine.Place(Region(w: 20, h: 40), "FIRE", opts);
|
|
|
|
Assert.Equal(10, p.FontSizePx, 6); // not shrunk
|
|
Assert.Equal(24, p.Width, 6); // overflows the 20-wide cell
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(OverlayHJust.Left, 0)]
|
|
[InlineData(OverlayHJust.Center, 44)] // (100-12)/2
|
|
[InlineData(OverlayHJust.Right, 88)] // 100-12
|
|
public void Place_HorizontalJustification(OverlayHJust hjust, double expectedX)
|
|
{
|
|
// "AB" at size 10 -> 12 x 12 inside a 100 x 20 cell at origin.
|
|
var opts = new OverlayLayoutOptions { HJust = hjust };
|
|
PlacedLabel p = Engine.Place(Region(w: 100, h: 20), "AB", opts);
|
|
Assert.Equal(expectedX, p.X, 6);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(OverlayVJust.Top, 0)]
|
|
[InlineData(OverlayVJust.Center, 4)] // (20-12)/2
|
|
[InlineData(OverlayVJust.Bottom, 8)] // 20-12
|
|
public void Place_VerticalJustification(OverlayVJust vjust, double expectedY)
|
|
{
|
|
var opts = new OverlayLayoutOptions { VJust = vjust };
|
|
PlacedLabel p = Engine.Place(Region(w: 100, h: 20), "AB", opts);
|
|
Assert.Equal(expectedY, p.Y, 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void Place_RegionOverride_BeatsEngineDefault()
|
|
{
|
|
OverlayRegion region = Region(w: 100, h: 20);
|
|
region.HJust = OverlayHJust.Right;
|
|
region.VJust = OverlayVJust.Top;
|
|
|
|
var opts = new OverlayLayoutOptions { HJust = OverlayHJust.Left, VJust = OverlayVJust.Bottom };
|
|
PlacedLabel p = Engine.Place(region, "AB", opts);
|
|
|
|
Assert.Equal(88, p.X, 6); // Right wins over Left
|
|
Assert.Equal(0, p.Y, 6); // Top wins over Bottom
|
|
}
|
|
|
|
[Fact]
|
|
public void Place_Rotated_CentersInCell_AndCarriesAngle()
|
|
{
|
|
// "HELLO" at size 10 -> 30 x 12; fits the swapped 200 x 50 envelope, so size holds.
|
|
OverlayRegion region = Region(w: 50, h: 200, size: 10);
|
|
region.RotationDegrees = 90;
|
|
|
|
PlacedLabel p = Engine.Place(region, "HELLO");
|
|
|
|
Assert.Equal(90, p.RotationDegrees, 6);
|
|
Assert.Equal(10, p.FontSizePx, 6);
|
|
Assert.Equal((50 - 30) / 2.0, p.X, 6); // centered horizontally -> 10
|
|
Assert.Equal((200 - 12) / 2.0, p.Y, 6); // centered vertically -> 94
|
|
}
|
|
|
|
[Fact]
|
|
public void Place_Rotated_FitsAgainstSwappedDimensions()
|
|
{
|
|
// A wide, short cell: upright the text fits height-bound; rotated it must fit
|
|
// the (swapped) narrow height, forcing a smaller font.
|
|
OverlayRegion upright = Region(w: 200, h: 20, size: 30);
|
|
OverlayRegion rotated = Region(w: 200, h: 20, size: 30);
|
|
rotated.RotationDegrees = 90;
|
|
|
|
double uprightSize = Engine.Place(upright, "HELLO").FontSizePx;
|
|
double rotatedSize = Engine.Place(rotated, "HELLO").FontSizePx;
|
|
|
|
Assert.True(rotatedSize < uprightSize,
|
|
$"rotated {rotatedSize} should be smaller than upright {uprightSize} (fit uses swapped axes)");
|
|
}
|
|
|
|
[Fact]
|
|
public void Layout_SkipsEmptyAndMissingLabels_PreservesRegionOrder()
|
|
{
|
|
var template = new OverlayTemplate
|
|
{
|
|
Regions =
|
|
{
|
|
Region("b-00"),
|
|
Region("b-01"),
|
|
Region("b-02"),
|
|
},
|
|
};
|
|
var labels = new Dictionary<string, string>
|
|
{
|
|
["b-00"] = "FIRE",
|
|
["b-01"] = "", // empty -> skipped
|
|
["b-02"] = "GEAR",
|
|
["b-99"] = "ORPHAN", // no such region -> ignored
|
|
};
|
|
|
|
IList<PlacedLabel> placed = Engine.Layout(template, labels);
|
|
|
|
Assert.Equal(2, placed.Count);
|
|
Assert.Equal("b-00", placed[0].RegionName);
|
|
Assert.Equal("b-02", placed[1].RegionName);
|
|
Assert.Equal("GEAR", placed[1].Text);
|
|
}
|
|
}
|