Files
CydandClaude Fable 5 1ff0b16015 Phase 8A (2/2): RioJoy.Core multi-targets net48 + net40 (Windows XP flavor)
- 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>
2026-07-11 20:41:58 -05:00

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);
}
}