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>
This commit is contained in:
@@ -10,10 +10,12 @@ namespace System.Collections.Generic
|
||||
/// <summary>
|
||||
/// Polyfill for CollectionExtensions.GetValueOrDefault (netstandard2.1+),
|
||||
/// absent on net48. Returns the value for <paramref name="key"/> or
|
||||
/// default(TValue) when the key is missing.
|
||||
/// default(TValue) when the key is missing. Takes IDictionary rather than
|
||||
/// IReadOnlyDictionary: net40 (Windows XP flavor) predates the IReadOnly*
|
||||
/// interfaces, so shared code uses the classic ones throughout.
|
||||
/// </summary>
|
||||
public static TValue? GetValueOrDefault<TKey, TValue>(
|
||||
this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key)
|
||||
this IDictionary<TKey, TValue> dictionary, TKey key)
|
||||
{
|
||||
if (dictionary is null) throw new ArgumentNullException(nameof(dictionary));
|
||||
return dictionary.TryGetValue(key, out var value) ? value : default;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace RioJoy.Core.Compat;
|
||||
|
||||
/// <summary>
|
||||
/// net48/net40 bridge for the handful of Task-era statics the code uses. On
|
||||
/// net40 (the Windows XP flavor) these come from Microsoft.Bcl.Async's
|
||||
/// <c>TaskEx</c>, and <c>SemaphoreSlim.WaitAsync</c> doesn't exist at all —
|
||||
/// there the wait blocks briefly instead, which at 9600 baud (tiny writes,
|
||||
/// rare contention) costs nothing measurable.
|
||||
/// </summary>
|
||||
internal static class TaskCompat
|
||||
{
|
||||
#if NET40
|
||||
public static Task Run(Action action) => TaskEx.Run(action);
|
||||
|
||||
public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) =>
|
||||
TaskEx.Delay(delay, cancellationToken);
|
||||
|
||||
public static Task<Task> WhenAny(IEnumerable<Task> tasks) => TaskEx.WhenAny(tasks);
|
||||
|
||||
public static Task WhenAll(IEnumerable<Task> tasks) => TaskEx.WhenAll(tasks);
|
||||
|
||||
public static Task WaitAsync(SemaphoreSlim semaphore, CancellationToken cancellationToken)
|
||||
{
|
||||
semaphore.Wait(cancellationToken); // net40: no WaitAsync; block (see class doc)
|
||||
return TaskEx.FromResult(true);
|
||||
}
|
||||
#else
|
||||
public static Task Run(Action action) => Task.Run(action);
|
||||
|
||||
public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) =>
|
||||
Task.Delay(delay, cancellationToken);
|
||||
|
||||
public static Task<Task> WhenAny(IEnumerable<Task> tasks) => Task.WhenAny(tasks);
|
||||
|
||||
public static Task WhenAll(IEnumerable<Task> tasks) => Task.WhenAll(tasks);
|
||||
|
||||
public static Task WaitAsync(SemaphoreSlim semaphore, CancellationToken cancellationToken) =>
|
||||
semaphore.WaitAsync(cancellationToken);
|
||||
#endif
|
||||
}
|
||||
@@ -27,7 +27,7 @@ public sealed record PanelGroup(
|
||||
bool LampCapable,
|
||||
int OriginCol,
|
||||
int OriginRow,
|
||||
IReadOnlyList<int?> Addresses);
|
||||
IList<int?> Addresses);
|
||||
|
||||
/// <summary>One address button placed on the panel (absolute cell coordinates).</summary>
|
||||
public sealed record PanelButton(int Address, PanelGroup Group, int Col, int Row, bool LampCapable);
|
||||
@@ -66,7 +66,7 @@ public static class CockpitPanel
|
||||
}
|
||||
|
||||
/// <summary>All panel groups, positioned for rendering.</summary>
|
||||
public static IReadOnlyList<PanelGroup> Groups { get; } = new[]
|
||||
public static IList<PanelGroup> Groups { get; } = new[]
|
||||
{
|
||||
// Upper MFD row.
|
||||
new PanelGroup("Upper Left MFD", PanelGroupKind.Mfd, 4, 2, true, 0, 1, Mfd(0x2F)),
|
||||
@@ -90,7 +90,7 @@ public static class CockpitPanel
|
||||
};
|
||||
|
||||
/// <summary>Flatten the groups to positioned <see cref="PanelButton"/>s (absolute cells).</summary>
|
||||
public static IReadOnlyList<PanelButton> Buttons()
|
||||
public static IList<PanelButton> Buttons()
|
||||
{
|
||||
var buttons = new List<PanelButton>();
|
||||
foreach (PanelGroup g in Groups)
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed record SheetCell(int Row, int Col, string Text, int? Address, Shee
|
||||
/// </summary>
|
||||
public static class SheetLayout
|
||||
{
|
||||
public static IReadOnlyList<SheetCell> Load(string path) => Parse(File.ReadAllText(path));
|
||||
public static IList<SheetCell> Load(string path) => Parse(File.ReadAllText(path));
|
||||
|
||||
/// <summary>
|
||||
/// The RIO address a cell's text encodes (two hex digits in 0x00–0x6F), or null.
|
||||
@@ -66,7 +66,7 @@ public static class SheetLayout
|
||||
/// longer than <paramref name="maxTextLength"/> is dropped as a merged-cell
|
||||
/// artifact; the clean export has none.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<SheetCell> Parse(string csv, int maxTextLength = 40)
|
||||
public static IList<SheetCell> Parse(string csv, int maxTextLength = 40)
|
||||
{
|
||||
if (csv is null) throw new ArgumentNullException(nameof(csv));
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ public readonly record struct KeyName(string Name, byte Value);
|
||||
public static class KeyCatalog
|
||||
{
|
||||
/// <summary>All catalogued keys, in menu order (letters, digits, F-keys, …).</summary>
|
||||
public static IReadOnlyList<KeyName> Entries { get; } = Build();
|
||||
public static IList<KeyName> Entries { get; } = Build();
|
||||
|
||||
private static readonly Dictionary<byte, string> ByValue =
|
||||
Entries.GroupBy(e => e.Value).ToDictionary(g => g.Key, g => g.First().Name);
|
||||
|
||||
@@ -14,7 +14,7 @@ public sealed class RioInputMap
|
||||
public RioInputMap() { }
|
||||
|
||||
/// <summary>Create a map from raw 16-bit words indexed by address.</summary>
|
||||
public RioInputMap(IReadOnlyDictionary<int, ushort> entries)
|
||||
public RioInputMap(IDictionary<int, ushort> entries)
|
||||
{
|
||||
if (entries is null) throw new ArgumentNullException(nameof(entries));
|
||||
foreach ((int address, ushort raw) in entries)
|
||||
|
||||
@@ -114,7 +114,7 @@ public sealed class HidFeederJoystickSink : IJoystickSink, IDisposable
|
||||
try
|
||||
{
|
||||
var ifData = new SP_DEVICE_INTERFACE_DATA();
|
||||
ifData.cbSize = Marshal.SizeOf<SP_DEVICE_INTERFACE_DATA>();
|
||||
ifData.cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DATA));
|
||||
|
||||
if (!SetupDiEnumDeviceInterfaces(devInfo, IntPtr.Zero, ref guid, 0, ref ifData))
|
||||
return null;
|
||||
|
||||
@@ -69,7 +69,7 @@ public sealed class SendInputSink : IInputSink
|
||||
}
|
||||
|
||||
private static void Send(INPUT input) =>
|
||||
SendInput(1, new[] { input }, Marshal.SizeOf<INPUT>());
|
||||
SendInput(1, new[] { input }, Marshal.SizeOf(typeof(INPUT)));
|
||||
|
||||
// --- Win32 interop --------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#if !NET40 // ViGEmBus is Win10+ only; the XP flavor uses RioGamepadXP (PLAN.md §8B)
|
||||
using Nefarius.ViGEm.Client;
|
||||
using Nefarius.ViGEm.Client.Targets;
|
||||
using Nefarius.ViGEm.Client.Targets.Xbox360;
|
||||
@@ -26,7 +27,7 @@ public sealed class ViGEmJoystickSink : IJoystickSink, IDisposable
|
||||
};
|
||||
|
||||
/// <summary>Display names for the mappable buttons, aligned with <see cref="ButtonMap"/>.</summary>
|
||||
public static IReadOnlyList<string> ButtonNames { get; } = new[]
|
||||
public static IList<string> ButtonNames { get; } = new[]
|
||||
{
|
||||
"A", "B", "X", "Y", "LB", "RB", "Back", "Start", "L3", "R3", "Guide",
|
||||
};
|
||||
@@ -125,3 +126,4 @@ public sealed class ViGEmJoystickSink : IJoystickSink, IDisposable
|
||||
_client.Dispose();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -18,8 +18,8 @@ public static class GoobieDataImporter
|
||||
{
|
||||
/// <summary>A parsed data file: the field names and one label map per row.</summary>
|
||||
public sealed record Sheet(
|
||||
IReadOnlyList<string> Fields,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, string>> Rows);
|
||||
IList<string> Fields,
|
||||
IList<IDictionary<string, string>> Rows);
|
||||
|
||||
public static Sheet Load(string path) => Parse(File.ReadAllText(path));
|
||||
|
||||
@@ -33,7 +33,7 @@ public static class GoobieDataImporter
|
||||
throw new FormatException("No lists found; expected a header list of field names.");
|
||||
|
||||
List<string> fields = lists[0];
|
||||
var rows = new List<IReadOnlyDictionary<string, string>>();
|
||||
var rows = new List<IDictionary<string, string>>();
|
||||
|
||||
for (int i = 1; i < lists.Count; i++)
|
||||
{
|
||||
@@ -51,7 +51,7 @@ public static class GoobieDataImporter
|
||||
/// Convenience: the label map for a single row (default the first), with empty
|
||||
/// values dropped — ready to hand to <see cref="OverlayLayoutEngine.Layout"/>.
|
||||
/// </summary>
|
||||
public static IReadOnlyDictionary<string, string> LabelsForRow(Sheet sheet, int rowIndex = 0)
|
||||
public static IDictionary<string, string> LabelsForRow(Sheet sheet, int rowIndex = 0)
|
||||
{
|
||||
if (sheet is null) throw new ArgumentNullException(nameof(sheet));
|
||||
if (rowIndex < 0 || rowIndex >= sheet.Rows.Count)
|
||||
|
||||
@@ -15,7 +15,7 @@ public static class OverlayHitTester
|
||||
/// A region's rectangle is its stored cell geometry; label rotation does not
|
||||
/// affect hit-testing.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<OverlayRegion> RegionsAt(OverlayTemplate template, double x, double y)
|
||||
public static IList<OverlayRegion> RegionsAt(OverlayTemplate template, double x, double y)
|
||||
{
|
||||
if (template is null) throw new ArgumentNullException(nameof(template));
|
||||
|
||||
@@ -33,7 +33,7 @@ public static class OverlayHitTester
|
||||
/// </summary>
|
||||
public static OverlayRegion? NextAt(OverlayTemplate template, double x, double y, string? currentName)
|
||||
{
|
||||
IReadOnlyList<OverlayRegion> hits = RegionsAt(template, x, y);
|
||||
IList<OverlayRegion> hits = RegionsAt(template, x, y);
|
||||
if (hits.Count == 0)
|
||||
return null;
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ public sealed class OverlayLayoutEngine
|
||||
/// in <paramref name="labels"/> (or with empty text) are skipped — matching the
|
||||
/// legacy behavior where a blank field produced no visible text.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PlacedLabel> Layout(
|
||||
public IList<PlacedLabel> Layout(
|
||||
OverlayTemplate template,
|
||||
IReadOnlyDictionary<string, string> labels,
|
||||
IDictionary<string, string> labels,
|
||||
OverlayLayoutOptions? options = null)
|
||||
{
|
||||
if (template is null) throw new ArgumentNullException(nameof(template));
|
||||
|
||||
@@ -33,7 +33,10 @@ public readonly struct SwitchDecision : IEquatable<SwitchDecision>
|
||||
|
||||
public bool Equals(SwitchDecision other) => Mode == other.Mode && ReferenceEquals(Profile, other.Profile);
|
||||
public override bool Equals(object? obj) => obj is SwitchDecision d && Equals(d);
|
||||
public override int GetHashCode() => HashCode.Combine(Mode, Profile);
|
||||
// Manual combine: HashCode.Combine needs Microsoft.Bcl.HashCode, which has
|
||||
// no net40 build (Windows XP flavor).
|
||||
public override int GetHashCode() =>
|
||||
((int)Mode * 397) ^ (Profile?.GetHashCode() ?? 0);
|
||||
public override string ToString() => Mode == SwitchMode.Activate ? $"Activate({Profile?.Name})" : Mode.ToString();
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ public sealed class AutoSwitchWatcher
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
|
||||
await Compat.TaskCompat.Delay(interval, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class IniFile
|
||||
}
|
||||
|
||||
/// <summary>All key/value pairs in a section (empty if the section is absent).</summary>
|
||||
public IReadOnlyDictionary<string, string> Section(string name) =>
|
||||
public IDictionary<string, string> Section(string name) =>
|
||||
_sections.TryGetValue(name, out Dictionary<string, string>? s)
|
||||
? s
|
||||
: new Dictionary<string, string>();
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- net48 test branch. Uses Win32 P/Invoke (SendInput, SystemParametersInfo)
|
||||
and System.IO.Ports. Span/records/init/Index are polyfilled (System.Memory
|
||||
+ PolySharp); System.Text.Json comes from NuGet (not in the net48 BCL). -->
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<!-- Two flavors (PLAN.md §Phase 8): net48 = Windows 10/11 (x64, ViGEm +
|
||||
RioGamepad); net40 = Windows XP SP3 (x86 cabinets — .NET 4.0 is XP's
|
||||
ceiling). Modern language features come from PolySharp on both; net40
|
||||
async machinery comes from Microsoft.Bcl.Async (TaskEx via
|
||||
Compat/TaskCompat). Win32 P/Invoke (SendInput, DeviceIoControl) and
|
||||
in-box System.IO.Ports work on both. -->
|
||||
<TargetFrameworks>net48;net40</TargetFrameworks>
|
||||
<Platforms>x64</Platforms>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- net48 pins x64 (matches the driver + tray exe); net40 stays AnyCPU and
|
||||
the XP tray exe pins x86. -->
|
||||
<PropertyGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net40'">
|
||||
<!-- net40 has no System.Net.Http assembly for the implicit global using. -->
|
||||
<Using Remove="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nefarius.ViGEm.Client" Version="1.21.256" />
|
||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||
<PackageReference Include="PolySharp" Version="1.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<PackageReference Include="Nefarius.ViGEm.Client" Version="1.21.256" />
|
||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net40'">
|
||||
<PackageReference Include="Microsoft.Bcl.Async" Version="1.0.168" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -69,19 +69,19 @@ public sealed class RioSerialLink
|
||||
{
|
||||
// If any running loop ends (transport closed / error / cancellation),
|
||||
// tear the others down too.
|
||||
await Task.WhenAny(loops).ConfigureAwait(false);
|
||||
await Compat.TaskCompat.WhenAny(loops).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
linked.Cancel();
|
||||
await Task.WhenAll(loops.Select(Swallow)).ConfigureAwait(false);
|
||||
await Compat.TaskCompat.WhenAll(loops.Select(Swallow)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Send a pre-built packet (see <see cref="PacketBuilder"/>) to the RIO.</summary>
|
||||
public async Task SendAsync(byte[] packet, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
await Compat.TaskCompat.WaitAsync(_writeLock, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await _transport.WriteAsync(packet, cancellationToken).ConfigureAwait(false);
|
||||
@@ -191,7 +191,7 @@ public sealed class RioSerialLink
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(_options.AnalogPollInterval, ct).ConfigureAwait(false);
|
||||
await Compat.TaskCompat.Delay(_options.AnalogPollInterval, ct).ConfigureAwait(false);
|
||||
|
||||
await RequestAnalogAsync(ct).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ public sealed class SerialPortTransport : IRioTransport
|
||||
// still releases the handle.
|
||||
}
|
||||
|
||||
Task close = Task.Run(() =>
|
||||
Task close = Compat.TaskCompat.Run(() =>
|
||||
{
|
||||
try { _port.Dispose(); }
|
||||
catch { /* best-effort release */ }
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class ProfileWallpaperGenerator
|
||||
public string Generate(
|
||||
OverlayTemplate template,
|
||||
string templateDirectory,
|
||||
IReadOnlyDictionary<string, string> labels,
|
||||
IDictionary<string, string> labels,
|
||||
string outputPath,
|
||||
OverlayLayoutOptions? options = null)
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class SkiaOverlayRenderer
|
||||
/// </summary>
|
||||
public SKBitmap Render(
|
||||
OverlayTemplate template,
|
||||
IReadOnlyDictionary<string, string> labels,
|
||||
IDictionary<string, string> labels,
|
||||
SKBitmap baseImage,
|
||||
OverlayLayoutOptions? options = null)
|
||||
{
|
||||
@@ -95,7 +95,7 @@ public sealed class SkiaOverlayRenderer
|
||||
/// <summary>Render and encode to PNG bytes.</summary>
|
||||
public byte[] RenderToPng(
|
||||
OverlayTemplate template,
|
||||
IReadOnlyDictionary<string, string> labels,
|
||||
IDictionary<string, string> labels,
|
||||
SKBitmap baseImage,
|
||||
OverlayLayoutOptions? options = null)
|
||||
{
|
||||
@@ -110,7 +110,7 @@ public sealed class SkiaOverlayRenderer
|
||||
/// </summary>
|
||||
public void RenderToFile(
|
||||
OverlayTemplate template,
|
||||
IReadOnlyDictionary<string, string> labels,
|
||||
IDictionary<string, string> labels,
|
||||
string outputPath,
|
||||
OverlayLayoutOptions? options = null)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace RioJoy.Tray.Editor;
|
||||
/// </summary>
|
||||
internal sealed class PanelCanvas : Control
|
||||
{
|
||||
private static readonly IReadOnlyList<PanelButton> AllButtons = CockpitPanel.Buttons();
|
||||
private static readonly IList<PanelButton> AllButtons = CockpitPanel.Buttons();
|
||||
|
||||
public PanelCanvas()
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@ public static class PanelView
|
||||
public const int CalOriginCol = 2;
|
||||
public const int CalOriginRow = 9;
|
||||
|
||||
public static IReadOnlyList<CalibrationCell> CalibrationCells { get; } = new[]
|
||||
public static IList<CalibrationCell> CalibrationCells { get; } = new[]
|
||||
{
|
||||
new CalibrationCell(JoyStickSetting.InvertX, "Inv X", 2, 10),
|
||||
new CalibrationCell(JoyStickSetting.InvertY, "Inv Y", 2, 11),
|
||||
@@ -51,7 +51,7 @@ public static class PanelView
|
||||
new CalibrationCell(JoyStickSetting.EnableZR, "ZR mix", 2, 16),
|
||||
};
|
||||
|
||||
public static Size GridSize(IReadOnlyList<PanelButton> buttons)
|
||||
public static Size GridSize(IList<PanelButton> buttons)
|
||||
{
|
||||
int maxCol = 0, maxRow = 0;
|
||||
foreach (PanelButton b in buttons)
|
||||
@@ -69,8 +69,8 @@ public static class PanelView
|
||||
|
||||
public static void Paint(
|
||||
Graphics g,
|
||||
IReadOnlyList<PanelButton> buttons,
|
||||
IReadOnlyList<PanelGroup> groups,
|
||||
IList<PanelButton> buttons,
|
||||
IList<PanelGroup> groups,
|
||||
Func<int, string?> label,
|
||||
Func<int, string?> function,
|
||||
Func<int, bool> lit,
|
||||
@@ -225,7 +225,7 @@ public static class PanelView
|
||||
}
|
||||
}
|
||||
|
||||
public static PanelButton? HitTest(IReadOnlyList<PanelButton> buttons, Point p)
|
||||
public static PanelButton? HitTest(IList<PanelButton> buttons, Point p)
|
||||
{
|
||||
foreach (PanelButton b in buttons)
|
||||
if (Cell(b.Col, b.Row).Contains(p))
|
||||
|
||||
@@ -50,7 +50,7 @@ public class CockpitPanelTests
|
||||
public void Keypad_PhysicalLayout_MapsDigitsToAddresses()
|
||||
{
|
||||
// Internal keypad: top-left "1" -> 0x51, bottom-middle "0" -> 0x50, "C" -> 0x5C.
|
||||
IReadOnlyList<PanelButton> all = CockpitPanel.Buttons();
|
||||
IList<PanelButton> all = CockpitPanel.Buttons();
|
||||
PanelGroup pad = CockpitPanel.Groups.Single(g => g.Title == "Internal Keypad");
|
||||
|
||||
Assert.Equal(0x51, pad.Addresses[0]); // row0 col0 = "1"
|
||||
|
||||
@@ -47,7 +47,7 @@ public class SheetLayoutTests
|
||||
"\"\",\"IsLit\",\"\",\"\",\"0F\"\n" +
|
||||
"\"\",\"\",\"\",\"\",\"HOME\"";
|
||||
|
||||
IReadOnlyList<SheetCell> cells = SheetLayout.Parse(csv);
|
||||
IList<SheetCell> cells = SheetLayout.Parse(csv);
|
||||
|
||||
SheetCell addr = Assert.Single(cells, c => c.Text == "0F");
|
||||
Assert.Equal(0x0F, addr.Address);
|
||||
@@ -64,7 +64,7 @@ public class SheetLayoutTests
|
||||
[Fact]
|
||||
public void Parse_RealSheet_HasFullAddressMap()
|
||||
{
|
||||
IReadOnlyList<SheetCell> cells = SheetLayout.Load(
|
||||
IList<SheetCell> cells = SheetLayout.Load(
|
||||
TestRepo.CustomBackground("config-sheet.csv"));
|
||||
|
||||
// Every RIO address 0x00..0x6F that exists should appear exactly once as a cell.
|
||||
|
||||
@@ -26,7 +26,7 @@ public class GoobieDataImporterTests
|
||||
[Fact]
|
||||
public void LabelsForRow_DropsEmptyValues()
|
||||
{
|
||||
IReadOnlyDictionary<string, string> labels =
|
||||
IDictionary<string, string> labels =
|
||||
GoobieDataImporter.LabelsForRow(GoobieDataImporter.Parse(Sample));
|
||||
|
||||
Assert.True(labels.ContainsKey("b-00"));
|
||||
|
||||
@@ -21,7 +21,7 @@ public class OverlayHitTesterTests
|
||||
[Fact]
|
||||
public void RegionsAt_ReturnsSmallestFirst()
|
||||
{
|
||||
IReadOnlyList<OverlayRegion> hits = OverlayHitTester.RegionsAt(Template(), 100, 50);
|
||||
IList<OverlayRegion> hits = OverlayHitTester.RegionsAt(Template(), 100, 50);
|
||||
|
||||
Assert.Equal(new[] { "small", "big" }, hits.Select(r => r.Name));
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ public class OverlayLayoutEngineTests
|
||||
["b-99"] = "ORPHAN", // no such region -> ignored
|
||||
};
|
||||
|
||||
IReadOnlyList<PlacedLabel> placed = Engine.Layout(template, labels);
|
||||
IList<PlacedLabel> placed = Engine.Layout(template, labels);
|
||||
|
||||
Assert.Equal(2, placed.Count);
|
||||
Assert.Equal("b-00", placed[0].RegionName);
|
||||
|
||||
@@ -35,7 +35,7 @@ public class OverlayRenderIntegrationTests
|
||||
Assert.Equal(template.Width, baseImg.Width);
|
||||
Assert.Equal(template.Height, baseImg.Height);
|
||||
|
||||
IReadOnlyDictionary<string, string> labels =
|
||||
IDictionary<string, string> labels =
|
||||
GoobieDataImporter.LabelsForRow(GoobieDataImporter.Load(TestRepo.CustomBackground("TEST.data")));
|
||||
|
||||
using SKBitmap rendered = new SkiaOverlayRenderer().Render(template, labels, baseImg);
|
||||
|
||||
Reference in New Issue
Block a user